1/** 2 * @file 3 * JavaScript behaviors for custom webform #states. 4 */ 5 6(function ($, Drupal, once) { 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),legend span.fieldset__label: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 $(once('webform-checkboxes-required', '.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 .each(function () { 328 var $element = $(this); 329 $element.find('input[type="checkbox"]').on('click', statesCheckboxesRequiredEventHandler); 330 setTimeout(function () {checkboxesRequired($element);}); 331 }); 332 } 333 }; 334 335 /** 336 * Adds HTML5 validation to required radios. 337 * 338 * @type {Drupal~behavior} 339 * 340 * @see https://www.drupal.org/project/webform/issues/2856795 341 */ 342 Drupal.behaviors.webformRadiosRequired = { 343 attach: function (context) { 344 $(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)) 345 .each(function () { 346 var $element = $(this); 347 setTimeout(function () {radiosRequired($element);}); 348 }); 349 } 350 }; 351 352 /** 353 * Adds HTML5 validation to required table select. 354 * 355 * @type {Drupal~behavior} 356 * 357 * @see https://www.drupal.org/project/webform/issues/2856795 358 */ 359 Drupal.behaviors.webformTableSelectRequired = { 360 attach: function (context) { 361 $(once('webform-tableselect-required','.js-webform-tableselect.required', context)) 362 .each(function () { 363 var $element = $(this); 364 var $tbody = $element.find('tbody'); 365 var isMultiple = $element.is('[multiple]'); 366 367 if (isMultiple) { 368 // Check all checkbox triggers checkbox 'change' event on 369 // select and deselect all. 370 // @see Drupal.tableSelect 371 $tbody.find('input[type="checkbox"]').on('click change', function () { 372 checkboxesRequired($tbody); 373 }); 374 } 375 376 setTimeout(function () { 377 isMultiple ? checkboxesRequired($tbody) : radiosRequired($element); 378 }); 379 }); 380 } 381 }; 382 383 /** 384 * Add HTML5 multiple checkboxes required validation. 385 * 386 * @param {jQuery} $element
387 * An jQuery object containing HTML5 radios. 388 * 389 * @see https://stackoverflow.com/a/37825072/145846 390 */ 391 function checkboxesRequired($element) { 392 var $firstCheckbox = $element.find('input[type="checkbox"]').first(); 393 var isChecked = $element.find('input[type="checkbox"]').is(':checked'); 394 toggleRequired($firstCheckbox, !isChecked); 395 copyRequireMessage($element, $firstCheckbox); 396 } 397 398 /** 399 * Add HTML5 radios required validation. 400 * 401 * @param {jQuery} $element 402 * An jQuery object containing HTML5 radios. 403 * 404 * @see https://www.drupal.org/project/webform/issues/2856795 405 */ 406 function radiosRequired($element) { 407 var $radios = $element.find('input[type="radio"]'); 408 var isRequired = $element.hasClass('required'); 409 toggleRequired($radios, isRequired); 410 copyRequireMessage($element, $radios); 411 } 412 413 /* ************************************************************************ */ 414 // Event handlers. 415 /* ************************************************************************ */ 416 417 /** 418 * Trigger #states API HTML5 multiple checkboxes required validation. 419 * 420 * @see https://stackoverflow.com/a/37825072/145846 421 */ 422 function statesCheckboxesRequiredEventHandler() { 423 var $element = $(this).closest('.js-webform-type-checkboxes, .js-webform-type-webform-checkboxes-other'); 424 checkboxesRequired($element); 425 } 426 427 /** 428 * Trigger an input's event handlers. 429 * 430 * @param {element} input 431 * An input. 432 */ 433 function triggerEventHandlers(input) { 434 var $input = $(input); 435 var type = input.type; 436 var tag = input.tagName.toLowerCase(); 437 // Add 'webform.states' as extra parameter to event handlers. 438 // @see Drupal.behaviors.webformUnsaved 439 var extraParameters = ['webform.states']; 440 if (type === 'checkbox' || type === 'radio') { 441 $input 442 .trigger('change', extraParameters) 443 .trigger('blur', extraParameters); 444 } 445 else if (tag === 'select') { 446 // Do not trigger the onchange event for Address element's country code 447 // when it is initialized. 448 // @see \Drupal\address\Element\Country 449 if ($input.closest('.webform-type-address').length) { 450 if (!$input.data('webform-states-address-initialized') 451 && $input.attr('autocomplete') === 'country' 452 && $input.val() === $input.find("option[selected]").attr('value')) { 453 return; 454 } 455 $input.data('webform-states-address-initialized', true); 456 } 457 458 $input 459 .trigger('change', extraParameters) 460 .trigger('blur', extraParameters); 461 } 462 else if (type !== 'submit' && type !== 'button' && type !== 'file') { 463 // Make sure input mask is removed and then reset when value is restored. 464 // @see https://www.drupal.org/project/webform/issues/3124155 465 // @see https://www.drupal.org/project/webform/issues/3202795 466 var hasInputMask = ($.fn.inputmask && $input.hasClass('js-webform-input-mask')); 467 hasInputMask && $input.inputmask('remove'); 468 469 $input 470 .trigger('input', extraParameters) 471 .trigger('change', extraParameters) 472 .trigger('keydown', extraParameters) 473 .trigger('keyup', extraParameters) 474 .trigger('blur', extraParameters); 475 476 hasInputMask && $input.inputmask(); 477 } 478 } 479 480 /* ************************************************************************ */ 481 // Backup and restore value functions. 482 /* ************************************************************************ */ 483 484 /** 485 * Backup an input's current value and required attribute 486 * 487 * @param {element} input 488 * An input. 489 */ 490 function backupValueAndRequired(input) { 491 var $input = $(input); 492 var type = input.type; 493 var tag = input.tagName.toLowerCase(); // Normalize case. 494 495 // Backup required. 496 if ($input.prop('required') && !$input.hasData('webform-required')) { 497 $input.data('webform-required', true); 498 } 499 500 // Backup value. 501 if (!$input.hasData('webform-value')) { 502 if (type === 'checkbox' || type === 'radio') { 503 $input.data('webform-value', $input.prop('checked')); 504 } 505 else if (tag === 'select') { 506 var values = []; 507 $input.find('option:selected').each(function (i, option) { 508 values[i] = option.value; 509 }); 510 $input.data('webform-value', values); 511 } 512 else if (type !== 'submit' && type !== 'button') { 513 $input.data('webform-value', input.value); 514 } 515 } 516 } 517 518 /** 519 * Restore an input's value and required attribute. 520 * 521 * @param {element} input 522 * An input. 523 */ 524 function restoreValueAndRequired(input) { 525 var $input = $(input); 526 527 // Restore value. 528 var value = $input.data('webform-value'); 529 if (typeof value !== 'undefined') { 530 var type = input.type; 531 var tag = input.tagName.toLowerCase(); // Normalize case. 532 533 if (type === 'checkbox' || type === 'radio') { 534 $input.prop('checked', value); 535 } 536 else if (tag === 'select') { 537 $.each(value, function (i, option_value) { 538 // Prevent "Syntax error, unrecognized expression" error by 539 // escaping single quotes. 540 // @see https://forum.jquery.com/topic/escape-characters-prior-to-using-selector 541 option_value = option_value.replace(/'/g, "\\\'"); 542 $input.find("option[value='" + option_value + "']").prop('selected', true); 543 }); 544 } 545 else if (type !== 'submit' && type !== 'button') { 546 input.value = value; 547 } 548 $input.removeData('webform-value'); 549 } 550 551 // Restore required. 552 var required = $input.data('webform-required'); 553 if (typeof required !== 'undefined') { 554 if (required) { 555 $input.prop('required', true); 556 } 557 $input.removeData('webform-required'); 558 } 559 } 560 561 /** 562 * Clear an input's value and required attributes. 563 * 564 * @param {element} input 565 * An input. 566 */ 567 function clearValueAndRequired(input) { 568 var $input = $(input); 569 570 // Check for #states no clear attribute.
571 // @see https://css-tricks.com/snippets/jquery/make-an-jquery-hasattr/ 572 if ($input.closest('[data-webform-states-no-clear]').length) { 573 return; 574 } 575 576 // Clear value. 577 var type = input.type; 578 var tag = input.tagName.toLowerCase(); // Normalize case. 579 if (type === 'checkbox' || type === 'radio') { 580 $input.prop('checked', false); 581 } 582 else if (tag === 'select') { 583 if ($input.find('option[value=""]').length) { 584 $input.val(''); 585 } 586 else { 587 input.selectedIndex = -1; 588 } 589 } 590 else if (type !== 'submit' && type !== 'button') { 591 input.value = (type === 'color') ? '#000000' : ''; 592 } 593 594 // Clear required. 595 $input.prop('required', false); 596 } 597 598 /* ************************************************************************ */ 599 // Helper functions. 600 /* ************************************************************************ */ 601 602 /** 603 * Toggle an input's required attributes. 604 * 605 * @param {element} $input 606 * An input. 607 * @param {boolean} required 608 * Is input required. 609 */ 610 function toggleRequired($input, required) { 611 var isCheckboxOrRadio = ($input.attr('type') === 'radio' || $input.attr('type') === 'checkbox'); 612 if (required) { 613 if (isCheckboxOrRadio) { 614 $input.attr({'required': 'required'}); 615 } 616 else { 617 $input.attr({'required': 'required', 'aria-required': 'true'}); 618 } 619 } 620 else { 621 if (isCheckboxOrRadio) { 622 $input.removeAttr('required'); 623 } 624 else { 625 $input.removeAttr('required aria-required'); 626 } 627 } 628 } 629 630 /** 631 * Copy the clientside_validation.module's message. 632 * 633 * @param {jQuery} $source 634 * The source element. 635 * @param {jQuery} $destination 636 * The destination element. 637 */ 638 function copyRequireMessage($source, $destination) { 639 if ($source.attr('data-msg-required')) { 640 $destination.attr('data-msg-required', $source.attr('data-msg-required')); 641 } 642 } 643 644})(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.