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