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