1/** 2 * @file 3 * JavaScript behaviors for custom webform #states. 4 */ 5 6(function ($, Drupal, once) { 7 Drupal.webform = Drupal.webform || {}; 8 Drupal.webform.states = Drupal.webform.states || {}; 9 Drupal.webform.states.slideDown = Drupal.webform.states.slideDown || {}; 10 Drupal.webform.states.slideDown.duration = 'slow'; 11 Drupal.webform.states.slideUp = Drupal.webform.states.slideUp || {}; 12 Drupal.webform.states.slideUp.duration = 'fast'; 13 14 /* ************************************************************************ */ 15 // jQuery functions. 16 /* ************************************************************************ */ 17 18 /** 19 * Check if an element has a specified data attribute. 20 * 21 * @param {string} data 22 * The data attribute name. 23 * 24 * @return {boolean} 25 * TRUE if an element has a specified data attribute. 26 */ 27 $.fn.hasData = function (data) { 28 return (typeof this.data(data) !== 'undefined'); 29 }; 30 31 /** 32 * Check if element is within the webform or not. 33 * 34 * @return {boolean} 35 * TRUE if element is within the webform. 36 */ 37 $.fn.isWebform = function () { 38 return $(this).closest('form.webform-submission-form, form[id^="webform"], form[data-is-webform]').length ? true : false; 39 }; 40 41 /** 42 * Check if element is to be treated as a webform element. 43 * 44 * @return {boolean} 45 * TRUE if element is to be treated as a webform element. 46 */ 47 $.fn.isWebformElement = function () { 48 return ($(this).isWebform() || $(this).closest('[data-is-webform-element]').length) ? true : false; 49 }; 50 51 /* ************************************************************************ */ 52 // Trigger. 53 /* ************************************************************************ */ 54 55 // The change event is triggered by cut-n-paste and select menus. 56 // Issue #2445271: #states element empty check not triggered on mouse 57 // based paste. 58 // @see https://www.drupal.org/node/2445271 59 Drupal.states.Trigger.states.empty.change = function change() { 60 return this.val() === ''; 61 }; 62 63 /* ************************************************************************ */ 64 // Dependents. 65 /* ************************************************************************ */ 66 67 // Adds pattern, less than, and greater than support to #state API. 68 // @see http://drupalsun.com/julia-evans/2012/03/09/extending-form-api-states-regular-expressions 69 Drupal.states.Dependent.comparisons.Object = function (reference, value) { 70 if ('pattern' in reference) { 71 return (new RegExp(reference['pattern'])).test(value); 72 } 73 else if ('!pattern' in reference) { 74 return !((new RegExp(reference['!pattern'])).test(value)); 75 } 76 else if ('less' in reference) { 77 return (value !== '' && parseFloat(reference['less']) > parseFloat(value)); 78 } 79 else if ('less_equal' in reference) { 80 return (value !== '' && parseFloat(reference['less_equal']) >= parseFloat(value)); 81 } 82 else if ('greater' in reference) { 83 return (value !== '' && parseFloat(reference['greater']) < parseFloat(value)); 84 } 85 else if ('greater_equal' in reference) { 86 return (value !== '' && parseFloat(reference['greater_equal']) <= parseFloat(value)); 87 } 88 else if ('between' in reference || '!between' in reference) { 89 if (value === '') { 90 return false; 91 } 92 93 var between = reference['between'] || reference['!between']; 94 var betweenParts = between.split(':'); 95 var greater = betweenParts[0]; 96 var less = (typeof betweenParts[1] !== 'undefined') ? betweenParts[1] : null; 97 var isGreaterThan = (greater === null || greater === '' || parseFloat(value) >= parseFloat(greater)); 98 var isLessThan = (less === null || less === '' || parseFloat(value) <= parseFloat(less)); 99 var result = (isGreaterThan && isLessThan); 100 return (reference['!between']) ? !result : result; 101 } 102 else { 103 return reference.indexOf(value) !== false; 104 } 105 }; 106 107 /* ************************************************************************ */ 108 // States events. 109 /* ************************************************************************ */ 110 111 var $document = $(document); 112 113 $document.on('state:required', function (e) { 114 if (e.trigger && $(e.target).isWebformElement()) { 115 var $target = $(e.target); 116 // Fix #required file upload. 117 // @see Issue #2860529: Conditional required File upload field don't work. 118 toggleRequired($target.find('input[type="file"]'), e.value); 119 120 // Fix #required for radios and likert. 121 // @see Issue #2856795: If radio buttons are required but not filled form is nevertheless submitted. 122 if ($target.is('.js-form-type-radios, .js-form-type-webform-radios-other, .js-webform-type-radios, .js-webform-type-webform-radios-other, .js-webform-type-webform-entity-radios, .webform-likert-table')) { 123 $target.toggleClass('required', e.value); 124 toggleRequired($target.find('input[type="radio"]'), e.value); 125 } 126 127 // Fix #required for checkboxes. 128 // @see Issue #2938414: Checkboxes don't support #states required. 129 // @see checkboxRequiredhandler 130 if ($target.is('.js-form-type-checkboxes, .js-form-type-webform-checkboxes-other, .js-webform-type-checkboxes, .js-webform-type-webform-checkboxes-other')) { 131 $target.toggleClass('required', e.value); 132 var $checkboxes = $target.find('input[type="checkbox"]'); 133 if (e.value) { 134 // Add event handler. 135 $checkboxes.on('click', statesCheckboxesRequiredEventHandler); 136 // Initialize and add required attribute. 137 checkboxesRequired($target); 138 } 139 else { 140 // Remove event handler. 141 $checkboxes.off('click', statesCheckboxesRequiredEventHandler); 142 // Remove required attribute. 143 toggleRequired($checkboxes, false); 144 } 145 } 146 147 // Fix #required for tableselect. 148 // @see Issue #3212581: Table select does not trigger client side validation 149 if ($target.is('.js-webform-tableselect')) { 150 $target.toggleClass('required', e.value); 151 var isMultiple = $target.is('[multiple]'); 152 if (isMultiple) { 153 // Checkboxes. 154 var $tbody = $target.find('tbody'); 155 var $checkboxes = $tbody.find('input[type="checkbox"]'); 156 copyRequireMessage($target, $checkboxes); 157 if (e.value) { 158 $checkboxes.on('click change', statesCheckboxesRequiredEventHandler); 159 checkboxesRequired($tbody); 160 } 161 else { 162 $checkboxes.off('click change ', statesCheckboxesRequiredEventHandler); 163 toggleRequired($tbody, false); 164 } 165 } 166 else { 167 // Radios. 168 var $radios = $target.find('input[type="radio"]'); 169 copyRequireMessage($target, $radios); 170 toggleRequired($radios, e.value); 171 } 172 } 173 174 // Fix required label for elements without the for attribute. 175 // @see Issue #3145300: Conditional Visible Select Other not working. 176 if ($target.is('.js-form-type-webform-select-other, .js-webform-type-webform-select-other')) { 177 var $select = $target.find('select'); 178 toggleRequired($select, e.value); 179 copyRequireMessage($target, $select); 180 } 181 if ($target.find('> label:not([for])').length) { 182 $target.find('> label').toggleClass('js-form-required form-required', e.value); 183 } 184 185 // Fix required label for checkboxes and radios. 186 // @see Issue #2938414: Checkboxes don't support #states required 187 // @see Issue #2731991: Setting required on radios marks all options required. 188 // @see Issue #2856315: Conditional Logic - Requiring Radios in a Fieldset. 189 // Fix #required for fieldsets. 190 // @see Issue #2977569: Hidden fieldsets that become visible with conditional logic cannot be made required. 191 if ($target.is('.js-webform-type-radios, .js-webform-type-checkboxes, fieldset')) { 192 $target.find('legend span.fieldset-legend:not(.visually-hidden),legend span.fieldset__label:not(.visually-hidden)').toggleClass('js-form-required form-required', e.value); 193 } 194 195 // Issue #2986017: Fieldsets shouldn't have required attribute. 196 if ($target.is('fieldset')) { 197 $target.removeAttr('required aria-required'); 198 } 199 } 200 }); 201 202 $document.on('state:checked', function (e) { 203 if (e.trigger) { 204 $(e.target).trigger('change'); 205 } 206 }); 207 208 $document.on('state:readonly', function (e) { 209 if (e.trigger && $(e.target).isWebformElement()) { 210 $(e.target).prop('readonly', e.value).closest('.js-form-item, .js-form-wrapper').toggleClass('webform-readonly', e.value).find('input, textarea').prop('readonly', e.value); 211 212 // Trigger webform:readonly. 213 $(e.target).trigger('webform:readonly') 214 .find('select, input, textarea, button').trigger('webform:readonly'); 215 } 216 }); 217 218 $document.on('state:visible state:visible-slide', function (e) { 219 if (e.trigger && $(e.target).isWebformElement()) { 220 if (e.value) { 221 $(':input', e.target).addBack().each(function () { 222 restoreValueAndRequired(this); 223 triggerEventHandlers(this); 224 }); 225 } 226 else { 227 // @see https://www.sitepoint.com/jquery-function-clear-form-data/ 228 $(':input', e.target).addBack().each(function () { 229 backupValueAndRequired(this); 230 clearValueAndRequired(this); 231 triggerEventHandlers(this); 232 }); 233 } 234 } 235 }); 236 237 $document.on('state:visible-slide', function (e) { 238 if (e.trigger && $(e.target).isWebformElement()) { 239 var effect = e.value ? 'slideDown' : 'slideUp'; 240 var duration = Drupal.webform.states[effect].duration; 241 $(e.target).closest('.js-form-item, .js-form-submit, .js-form-wrapper')[effect](duration); 242 } 243 }); 244 Drupal.states.State.aliases['invisible-slide'] = '!visible-slide'; 245 246 $document.on('state:disabled', function (e) { 247 if (e.trigger && $(e.target).isWebformElement()) { 248 // Make sure disabled property is set before triggering webform:disabled. 249 // Copied from: core/misc/states.js 250 $(e.target) 251 .prop('disabled', e.value) 252 .closest('.js-form-item, .js-form-submit, .js-form-wrapper').toggleClass('form-disabled', e.value) 253 .find('select, input, textarea, button').prop('disabled', e.value); 254 255 // Never disable hidden file[fids] because the existing values will 256 // be completely lost when the webform is submitted. 257 var fileElements = $(e.target) 258 .find(':input[type="hidden"][name$="[fids]"]'); 259 if (fileElements.length) { 260 // Remove 'disabled' attribute from fieldset which will block 261 // all disabled elements from being submitted. 262 if ($(e.target).is('fieldset')) { 263 $(e.target).prop('disabled', false); 264 } 265 fileElements.removeAttr('disabled'); 266 } 267 268 // Trigger webform:disabled. 269 $(e.target).trigger('webform:disabled') 270 .find('select, input, textarea, button').trigger('webform:disabled'); 271 } 272 }); 273 274 /* ************************************************************************ */ 275 // Behaviors. 276 /* ************************************************************************ */ 277 278 /** 279 * Adds HTML5 validation to required checkboxes. 280 * 281 * @type {Drupal~behavior} 282 * 283 * @see https://www.drupal.org/project/webform/issues/3068998 284 */ 285 Drupal.behaviors.webformCheckboxesRequired = { 286 attach(context) { 287 $(once('webform-checkboxes-required', '.js-form-type-checkboxes.required, .webform-term-checkboxes.required, .js-form-type-webform-checkboxes-other.required, .js-webform-type-checkboxes.required, .js-webform-type-webform-checkboxes-other.required, .js-webform-type-webform-radios-other.checkboxes', context)) 288 .each(function () { 289 var $element = $(this); 290 $element.find('input[type="checkbox"]').on('click', statesCheckboxesRequiredEventHandler); 291 setTimeout(function () {checkboxesRequired($element);}); 292 }); 293 } 294 }; 295 296 /** 297 * Adds HTML5 validation to required radios. 298 * 299 * @type {Drupal~behavior} 300 * 301 * @see https://www.drupal.org/project/webform/issues/2856795 302 */ 303 Drupal.behaviors.webformRadiosRequired = { 304 attach(context) { 305 $(once('webform-radios-required', '.js-form-type-radios, .js-form-type-webform-radios-other, .js-webform-type-radios, .js-webform-type-webform-radios-other, .js-webform-type-webform-entity-radios, .js-webform-type-webform-scale', context)) 306 .each(function () { 307 var $element = $(this); 308 setTimeout(function () {radiosRequired($element);}); 309 }); 310 } 311 }; 312 313 /** 314 * Adds HTML5 validation to required table select. 315 * 316 * @type {Drupal~behavior} 317 * 318 * @see https://www.drupal.org/project/webform/issues/2856795 319 */ 320 Drupal.behaviors.webformTableSelectRequired = { 321 attach(context) { 322 $(once('webform-tableselect-required','.js-webform-tableselect.required', context)) 323 .each(function () { 324 var $element = $(this); 325 var $tbody = $element.find('tbody'); 326 var isMultiple = $element.is('[multiple]'); 327 328 if (isMultiple) { 329 // Check all checkbox triggers checkbox 'change' event on 330 // select and deselect all. 331 // @see Drupal.tableSelect 332 $tbody.find('input[type="checkbox"]').on('click change', function () { 333 checkboxesRequired($tbody); 334 }); 335 } 336 337 setTimeout(function () { 338 isMultiple ? checkboxesRequired($tbody) : radiosRequired($element); 339 }); 340 }); 341 } 342 }; 343 344 /** 345 * Add HTML5 multiple checkboxes required validation. 346 * 347 * @param {jQuery} $element
348 * An jQuery object containing HTML5 radios. 349 * 350 * @see https://stackoverflow.com/a/37825072/145846 351 */ 352 function checkboxesRequired($element) { 353 var $firstCheckbox = $element.find('input[type="checkbox"]').first(); 354 var isChecked = $element.find('input[type="checkbox"]').is(':checked'); 355 toggleRequired($firstCheckbox, !isChecked); 356 copyRequireMessage($element, $firstCheckbox); 357 } 358 359 /** 360 * Add HTML5 radios required validation. 361 * 362 * @param {jQuery} $element 363 * An jQuery object containing HTML5 radios. 364 * 365 * @see https://www.drupal.org/project/webform/issues/2856795 366 */ 367 function radiosRequired($element) { 368 var $radios = $element.find('input[type="radio"]'); 369 var isRequired = $element.hasClass('required'); 370 toggleRequired($radios, isRequired); 371 copyRequireMessage($element, $radios); 372 } 373 374 /* ************************************************************************ */ 375 // Event handlers. 376 /* ************************************************************************ */ 377 378 /** 379 * Trigger #states API HTML5 multiple checkboxes required validation. 380 * 381 * @see https://stackoverflow.com/a/37825072/145846 382 */ 383 function statesCheckboxesRequiredEventHandler() { 384 var $element = $(this).closest('.js-webform-type-checkboxes, .js-webform-type-webform-checkboxes-other, .js-webform-type-webform-term-checkboxes, .js-webform-tableselect tbody'); 385 checkboxesRequired($element); 386 } 387 388 /** 389 * Trigger an input's event handlers. 390 * 391 * @param {element} input 392 * An input. 393 */ 394 function triggerEventHandlers(input) { 395 var $input = $(input); 396 var type = input.type; 397 var tag = input.tagName.toLowerCase(); 398 // Add 'webform.states' as extra parameter to event handlers. 399 // @see Drupal.behaviors.webformUnsaved 400 var extraParameters = ['webform.states']; 401 if (type === 'checkbox' || type === 'radio') { 402 $input 403 .trigger('change', extraParameters) 404 .trigger('blur', extraParameters); 405 } 406 else if (tag === 'select') { 407 // Do not trigger the onchange event for Address element's country code 408 // when it is initialized. 409 // @see \Drupal\address\Element\Country 410 if ($input.closest('.webform-type-address').length) { 411 if (!$input.data('webform-states-address-initialized') 412 && $input.attr('autocomplete') === 'country' 413 && $input.val() === $input.find("option[selected]").attr('value')) { 414 return; 415 } 416 $input.data('webform-states-address-initialized', true); 417 } 418 419 $input 420 .trigger('change', extraParameters) 421 .trigger('blur', extraParameters); 422 } 423 else if (type !== 'submit' && type !== 'button' && type !== 'file') { 424 // Make sure input mask is removed and then reset when value is restored. 425 // @see https://www.drupal.org/project/webform/issues/3124155 426 // @see https://www.drupal.org/project/webform/issues/3202795 427 var hasInputMask = ($.fn.inputmask && $input.hasClass('js-webform-input-mask')); 428 hasInputMask && $input.inputmask('remove'); 429 430 $input 431 .trigger('input', extraParameters) 432 .trigger('change', extraParameters) 433 .trigger('keydown', extraParameters) 434 .trigger('keyup', extraParameters) 435 .trigger('blur', extraParameters); 436 437 hasInputMask && $input.inputmask(); 438 } 439 } 440 441 /* ************************************************************************ */ 442 // Backup and restore value functions. 443 /* ************************************************************************ */ 444 445 /** 446 * Backup an input's current value and required attribute 447 * 448 * @param {element} input 449 * An input. 450 */ 451 function backupValueAndRequired(input) { 452 var $input = $(input); 453 var type = input.type; 454 var tag = input.tagName.toLowerCase(); // Normalize case. 455 456 // Backup required. 457 if ($input.prop('required') && !$input.hasData('webform-required')) { 458 $input.data('webform-required', true); 459 } 460 461 // Backup value. 462 if (!$input.hasData('webform-value')) { 463 if (type === 'checkbox' || type === 'radio') { 464 $input.data('webform-value', $input.prop('checked')); 465 } 466 else if (tag === 'select') { 467 var values = []; 468 $input.find('option:selected').each(function (i, option) { 469 values[i] = option.value; 470 }); 471 $input.data('webform-value', values); 472 } 473 else if (type !== 'submit' && type !== 'button') { 474 $input.data('webform-value', input.value); 475 } 476 } 477 } 478 479 /** 480 * Restore an input's value and required attribute. 481 * 482 * @param {element} input 483 * An input. 484 */ 485 function restoreValueAndRequired(input) { 486 var $input = $(input); 487 488 // Restore value. 489 var value = $input.data('webform-value'); 490 if (typeof value !== 'undefined') { 491 var type = input.type; 492 var tag = input.tagName.toLowerCase(); // Normalize case. 493 494 if (type === 'checkbox' || type === 'radio') { 495 $input.prop('checked', value); 496 } 497 else if (tag === 'select') { 498 $.each(value, function (i, option_value) { 499 // Prevent "Syntax error, unrecognized expression" error by 500 // escaping single quotes. 501 // @see https://forum.jquery.com/topic/escape-characters-prior-to-using-selector 502 option_value = option_value.replace(/'/g, "\\\'"); 503 $input.find("option[value='" + option_value + "']").prop('selected', true); 504 }); 505 } 506 else if (type !== 'submit' && type !== 'button') { 507 input.value = value; 508 } 509 $input.removeData('webform-value'); 510 } 511 512 // Restore required. 513 var required = $input.data('webform-required'); 514 if (typeof required !== 'undefined') { 515 if (required) { 516 $input.prop('required', true); 517 } 518 $input.removeData('webform-required'); 519 } 520 } 521 522 /** 523 * Clear an input's value and required attributes. 524 * 525 * @param {element} input 526 * An input. 527 */ 528 function clearValueAndRequired(input) { 529 var $input = $(input); 530 531 // Check for #states no clear attribute.
532 // @see https://css-tricks.com/snippets/jquery/make-an-jquery-hasattr/ 533 if ($input.closest('[data-webform-states-no-clear]').length) { 534 return; 535 } 536 537 // Clear value. 538 var type = input.type; 539 var tag = input.tagName.toLowerCase(); // Normalize case. 540 if (type === 'checkbox' || type === 'radio') { 541 $input.prop('checked', false); 542 } 543 else if (tag === 'select') { 544 if ($input.find('option[value=""]').length) { 545 $input.val(''); 546 } 547 else { 548 input.selectedIndex = -1; 549 } 550 } 551 else if (type !== 'submit' && type !== 'button') { 552 input.value = (type === 'color') ? '#000000' : ''; 553 } 554 555 // Clear required. 556 $input.prop('required', false); 557 } 558 559 /* ************************************************************************ */ 560 // Helper functions. 561 /* ************************************************************************ */ 562 563 /** 564 * Toggle an input's required attributes. 565 * 566 * @param {element} $input 567 * An input. 568 * @param {boolean} required 569 * Is input required. 570 */ 571 function toggleRequired($input, required) { 572 var isCheckboxOrRadio = ($input.attr('type') === 'radio' || $input.attr('type') === 'checkbox'); 573 if (required) { 574 if (isCheckboxOrRadio) { 575 $input.attr({'required': 'required'}); 576 } 577 else { 578 $input.attr({'required': 'required', 'aria-required': 'true'}); 579 } 580 } 581 else { 582 if (isCheckboxOrRadio) { 583 $input.removeAttr('required'); 584 } 585 else { 586 $input.removeAttr('required aria-required'); 587 } 588 // Clear the validation state for the input. 589 // @see Drupal.behaviors.webformRequiredError 590 $input.each(function () {this.setCustomValidity && this.setCustomValidity('')}); 591 } 592 } 593 594 /** 595 * Copy the clientside_validation.module's message. 596 * 597 * @param {jQuery} $source 598 * The source element. 599 * @param {jQuery} $destination 600 * The destination element. 601 */ 602 function copyRequireMessage($source, $destination) { 603 if ($source.attr('data-msg-required')) { 604 $destination.attr('data-msg-required', $source.attr('data-msg-required')); 605 } 606 } 607 608})(jQuery, Drupal, once);
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.