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