PageSourceSearch

https://www.sanjose.org/modules/contrib/webform/js/webform.states.js?tm8p22

js sanjose.org collected 2026-10-02 03:42:22 UTC 23,424 bytes, 645 lines download raw bytes

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.