/** * @file * JavaScript behaviors for custom webform #states. */ (function ($, Drupal) { 'use strict'; Drupal.webform = Drupal.webform || {}; Drupal.webform.states = Drupal.webform.states || {}; Drupal.webform.states.slideDown = Drupal.webform.states.slideDown || {}; Drupal.webform.states.slideDown.duration = 'slow'; Drupal.webform.states.slideUp = Drupal.webform.states.slideUp || {}; Drupal.webform.states.slideUp.duration = 'fast'; /** * Check if an element has a specified data attribute. * * @param {string} data * The data attribute name. * * @return {boolean} * TRUE if an element has a specified data attribute. */ $.fn.hasData = function (data) { return (typeof this.data(data) !== 'undefined'); }; /** * Check if element is within the webform or not. * * @return {boolean} * TRUE if element is within the webform. */ $.fn.isWebform = function () { return $(this).closest('form[id^="webform"]').length ? true : false; }; // The change event is triggered by cut-n-paste and select menus. // Issue #2445271: #states element empty check not triggered on mouse // based paste. // @see https://www.drupal.org/node/2445271 Drupal.states.Trigger.states.empty.change = function change() { return this.val() === ''; }; // Apply solution included in #1962800 patch. // Issue #1962800: Form #states not working with literal integers as // values in IE11. // @see https://www.drupal.org/project/drupal/issues/1962800 // @see https://www.drupal.org/files/issues/core-states-not-working-with-integers-ie11_1962800_46.patch // // This issue causes pattern, less than, and greater than support to break. // @see https://www.drupal.org/project/webform/issues/2981724 var states = Drupal.states; Drupal.states.Dependent.prototype.compare = function compare(reference, selector, state) { var value = this.values[selector][state.name]; var name = reference.constructor.name; if (!name) { name = $.type(reference); name = name.charAt(0).toUpperCase() + name.slice(1); } if (name in states.Dependent.comparisons) { return states.Dependent.comparisons[name](reference, value); } if (reference.constructor.name in states.Dependent.comparisons) { return states.Dependent.comparisons[reference.constructor.name](reference, value); } return _compare2(reference, value); }; function _compare2(a, b) { if (a === b) { return typeof a === 'undefined' ? a : true; } return typeof a === 'undefined' || typeof b === 'undefined'; } // Adds pattern, less than, and greater than support to #state API. // @see http://drupalsun.com/julia-evans/2012/03/09/extending-form-api-states-regular-expressions Drupal.states.Dependent.comparisons.Object = function (reference, value) { if ('pattern' in reference) { return (new RegExp(reference['pattern'])).test(value); } else if ('!pattern' in reference) { return !((new RegExp(reference['!pattern'])).test(value)); } else if ('less' in reference) { return (value !== '' && parseFloat(reference['less']) > parseFloat(value)); } else if ('greater' in reference) { return (value !== '' && parseFloat(reference['greater']) < parseFloat(value)); } else if ('between' in reference) { if (value === '') { return false; } else { var between = reference['between']; var betweenParts = between.split(':'); var greater = betweenParts[0]; var less = (typeof betweenParts[1] !== 'undefined') ? betweenParts[1] : null; var isGreaterThan = (greater === null || greater === '' || parseFloat(value) >= parseFloat(greater)); var isLessThan = (less === null || less === '' || parseFloat(value) <= parseFloat(less)); return (isGreaterThan && isLessThan); } } else { return reference.indexOf(value) !== false; } }; var $document = $(document); $document.on('state:required', function (e) { if (e.trigger && $(e.target).isWebform()) { var $target = $(e.target); // Fix #required file upload. // @see Issue #2860529: Conditional required File upload field don't work. if (e.value) { $target.find('input[type="file"]').attr({'required': 'required', 'aria-required': 'true'}); } else { $target.find('input[type="file"]').removeAttr('required aria-required'); } // Fix required label for checkboxes and radios. // @see Issue #2938414: Checkboxes don't support #states required // @see Issue #2731991: Setting required on radios marks all options required. // @see Issue #2856315: Conditional Logic - Requiring Radios in a Fieldset. // Fix #required for fieldsets. // @see Issue #2977569: Hidden fieldsets that become visible with conditional logic cannot be made required. if ($target.is('.js-webform-type-radios, .js-webform-type-checkboxes, fieldset')) { if (e.value) { $target.find('legend span.fieldset-legend:not(.visually-hidden)').addClass('js-form-required form-required'); } else { $target.find('legend span.fieldset-legend:not(.visually-hidden)').removeClass('js-form-required form-required'); } } // Fix #required for radios. // @see Issue #2856795: If radio buttons are required but not filled form is nevertheless submitted. if ($target.is('.js-webform-type-radios, .js-form-type-webform-radios-other')) { if (e.value) { $target.find('input[type="radio"]').attr({'required': 'required', 'aria-required': 'true'}); } else { $target.find('input[type="radio"]').removeAttr('required aria-required'); } } // Fix #required for checkboxes. // @see Issue #2938414: Checkboxes don't support #states required. // @see checkboxRequiredhandler if ($target.is('.js-webform-type-checkboxes, .js-form-type-webform-checkboxes-other')) { var $checkboxes = $target.find('input[type="checkbox"]'); if (e.value) { // Bind the event handler and add custom HTML5 required validation // to all checkboxes. $checkboxes.bind('click', checkboxRequiredhandler); if (!$checkboxes.is(':checked')) { $checkboxes.attr({'required': 'required', 'aria-required': 'true'}); } } else { // Remove custom HTML5 required validation from all checkboxes // and unbind the event handler. $checkboxes .removeAttr('required aria-required') .unbind('click', checkboxRequiredhandler); } } // Issue #2986017: Fieldsets shouldn't have required attribute. if ($target.is('fieldset')) { $target.removeAttr('required aria-required'); } } }); $document.on('state:checked', function (e) { if (e.trigger) { $(e.target).change(); } }); $document.on('state:readonly', function (e) { if (e.trigger && $(e.target).isWebform()) { $(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); // Trigger webform:readonly. $(e.target).trigger('webform:readonly') .find('select, input, textarea, button').trigger('webform:readonly'); } }); $document.on('state:visible state:visible-slide', function (e) { if (e.trigger && $(e.target).isWebform()) { if (e.value) { $(':input', e.target).addBack().each(function () { restoreValueAndRequired(this); triggerEventHandlers(this); }); } else { // @see https://www.sitepoint.com/jquery-function-clear-form-data/ $(':input', e.target).addBack().each(function () { backupValueAndRequired(this); clearValueAndRequired(this); triggerEventHandlers(this); }); } } }); $document.bind('state:visible-slide', function (e) { if (e.trigger && $(e.target).isWebform()) { var effect = e.value ? 'slideDown' : 'slideUp'; var duration = Drupal.webform.states[effect].duration; $(e.target).closest('.js-form-item, .js-form-submit, .js-form-wrapper')[effect](duration); } }); Drupal.states.State.aliases['invisible-slide'] = '!visible-slide'; $document.on('state:disabled', function (e) { if (e.trigger && $(e.target).isWebform()) { // Make sure disabled property is set before triggering webform:disabled. // Copied from: core/misc/states.js $(e.target) .prop('disabled', e.value) .closest('.js-form-item, .js-form-submit, .js-form-wrapper').toggleClass('form-disabled', e.value) .find('select, input, textarea, button').prop('disabled', e.value); // Never disable hidden file[fids] because the existing values will // be completely lost when the webform is submitted. var fileElements = $(e.target) .find(':input[type="hidden"][name$="[fids]"]'); if (fileElements.length) { // Remove 'disabled' attribute from fieldset which will block // all disabled elements from being submitted. if ($(e.target).is('fieldset')) { $(e.target).prop('disabled', false); } fileElements.removeAttr('disabled'); } // Trigger webform:disabled. $(e.target).trigger('webform:disabled') .find('select, input, textarea, button').trigger('webform:disabled'); } }); /** * Trigger custom HTML5 multiple checkboxes validation. * * @see https://stackoverflow.com/a/37825072/145846 */ function checkboxRequiredhandler() { var $checkboxes = $(this).closest('.js-webform-type-checkboxes, .js-form-type-webform-checkboxes-other').find('input[type="checkbox"]'); if ($checkboxes.is(':checked')) { $checkboxes.removeAttr('required aria-required'); } else { $checkboxes.attr({'required': 'required', 'aria-required': 'true'}); } } /** * Trigger an input's event handlers. * * @param {element} input * An input. */ function triggerEventHandlers(input) { var $input = $(input); var type = input.type; var tag = input.tagName.toLowerCase(); // Add 'webform.states' as extra parameter to event handlers. // @see Drupal.behaviors.webformUnsaved var extraParameters = ['webform.states']; if (type === 'checkbox' || type === 'radio') { $input .trigger('change', extraParameters) .trigger('blur', extraParameters); } else if (tag === 'select') { $input .trigger('change', extraParameters) .trigger('blur', extraParameters); } else if (type !== 'submit' && type !== 'button' && type !== 'file') { $input .trigger('input', extraParameters) .trigger('change', extraParameters) .trigger('keydown', extraParameters) .trigger('keyup', extraParameters) .trigger('blur', extraParameters); // Make sure input mask is reset when value is restored. // @see https://www.drupal.org/project/webform/issues/3124155 if ($input.attr('data-inputmask-mask')) { setTimeout(function () {$input.inputmask('remove').inputmask();}); } } } /** * Backup an input's current value and required attribute * * @param {element} input * An input. */ function backupValueAndRequired(input) { var $input = $(input); var type = input.type; var tag = input.tagName.toLowerCase(); // Normalize case. // Backup required. if ($input.prop('required') && !$input.hasData('webform-required')) { $input.data('webform-required', true); } // Backup value. if (!$input.hasData('webform-value')) { if (type === 'checkbox' || type === 'radio') { $input.data('webform-value', $input.prop('checked')); } else if (tag === 'select') { var values = []; $input.find('option:selected').each(function (i, option) { values[i] = option.value; }); $input.data('webform-value', values); } else if (type !== 'submit' && type !== 'button') { $input.data('webform-value', input.value); } } } /** * Restore an input's value and required attribute. * * @param {element} input * An input. */ function restoreValueAndRequired(input) { var $input = $(input); // Restore value. var value = $input.data('webform-value'); if (typeof value !== 'undefined') { var type = input.type; var tag = input.tagName.toLowerCase(); // Normalize case. if (type === 'checkbox' || type === 'radio') { $input.prop('checked', value); } else if (tag === 'select') { $.each(value, function (i, option_value) { $input.find("option[value='" + option_value + "']").prop('selected', true); }); } else if (type !== 'submit' && type !== 'button') { input.value = value; } $input.removeData('webform-value'); } // Restore required. var required = $input.data('webform-required'); if (typeof required !== 'undefined') { if (required) { $input.prop('required', true); } $input.removeData('webform-required'); } } /** * Clear an input's value and required attributes. * * @param {element} input * An input. */ function clearValueAndRequired(input) { var $input = $(input); // Check for #states no clear attribute. // @see https://css-tricks.com/snippets/jquery/make-an-jquery-hasattr/ if ($input.closest('[data-webform-states-no-clear]').length) { return; } // Clear value. var type = input.type; var tag = input.tagName.toLowerCase(); // Normalize case. if (type === 'checkbox' || type === 'radio') { $input.prop('checked', false); } else if (tag === 'select') { if ($input.find('option[value=""]').length) { $input.val(''); } else { input.selectedIndex = -1; } } else if (type !== 'submit' && type !== 'button') { input.value = (type === 'color') ? '#000000' : ''; } // Clear required. $input.prop('required', false); } })(jQuery, Drupal);