website/modules/contrib/webform/js/webform.statesdfb4.js
2023-09-30 09:40:37 +02:00

648 lines
23 KiB
JavaScript

/**
* @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';
/* ************************************************************************ */
// jQuery functions.
/* ************************************************************************ */
/**
* 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.webform-submission-form, form[id^="webform"], form[data-is-webform]').length ? true : false;
};
/**
* Check if element is to be treated as a webform element.
*
* @return {boolean}
* TRUE if element is to be treated as a webform element.
*/
$.fn.isWebformElement = function () {
return ($(this).isWebform() || $(this).closest('[data-is-webform-element]').length) ? true : false;
};
/* ************************************************************************ */
// Trigger.
/* ************************************************************************ */
// 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() === '';
};
/* ************************************************************************ */
// Dependents.
/* ************************************************************************ */
// 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 ('less_equal' in reference) {
return (value !== '' && parseFloat(reference['less_equal']) >= parseFloat(value));
}
else if ('greater' in reference) {
return (value !== '' && parseFloat(reference['greater']) < parseFloat(value));
}
else if ('greater_equal' in reference) {
return (value !== '' && parseFloat(reference['greater_equal']) <= parseFloat(value));
}
else if ('between' in reference || '!between' in reference) {
if (value === '') {
return false;
}
var between = reference['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));
var result = (isGreaterThan && isLessThan);
return (reference['!between']) ? !result : result;
}
else {
return reference.indexOf(value) !== false;
}
};
/* ************************************************************************ */
// States events.
/* ************************************************************************ */
var $document = $(document);
$document.on('state:required', function (e) {
if (e.trigger && $(e.target).isWebformElement()) {
var $target = $(e.target);
// Fix #required file upload.
// @see Issue #2860529: Conditional required File upload field don't work.
toggleRequired($target.find('input[type="file"]'), e.value);
// Fix #required for radios and likert.
// @see Issue #2856795: If radio buttons are required but not filled form is nevertheless submitted.
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')) {
$target.toggleClass('required', e.value);
toggleRequired($target.find('input[type="radio"]'), e.value);
}
// Fix #required for checkboxes.
// @see Issue #2938414: Checkboxes don't support #states required.
// @see checkboxRequiredhandler
if ($target.is('.js-form-type-checkboxes, .js-form-type-webform-checkboxes-other, .js-webform-type-checkboxes, .js-webform-type-webform-checkboxes-other')) {
$target.toggleClass('required', e.value);
var $checkboxes = $target.find('input[type="checkbox"]');
if (e.value) {
// Add event handler.
$checkboxes.on('click', statesCheckboxesRequiredEventHandler);
// Initialize and add required attribute.
checkboxesRequired($target);
}
else {
// Remove event handler.
$checkboxes.off('click', statesCheckboxesRequiredEventHandler);
// Remove required attribute.
toggleRequired($checkboxes, false);
}
}
// Fix #required for tableselect.
// @see Issue #3212581: Table select does not trigger client side validation
if ($target.is('.js-webform-tableselect')) {
$target.toggleClass('required', e.value);
var isMultiple = $target.is('[multiple]');
if (isMultiple) {
// Checkboxes.
var $tbody = $target.find('tbody');
var $checkboxes = $tbody.find('input[type="checkbox"]');
copyRequireMessage($target, $checkboxes);
if (e.value) {
$checkboxes.on('click change', statesCheckboxesRequiredEventHandler);
checkboxesRequired($tbody);
}
else {
$checkboxes.off('click change ', statesCheckboxesRequiredEventHandler);
toggleRequired($tbody, false);
}
}
else {
// Radios.
var $radios = $target.find('input[type="radio"]');
copyRequireMessage($target, $radios);
toggleRequired($radios, e.value);
}
}
// Fix required label for elements without the for attribute.
// @see Issue #3145300: Conditional Visible Select Other not working.
if ($target.is('.js-form-type-webform-select-other, .js-webform-type-webform-select-other')) {
var $select = $target.find('select');
toggleRequired($select, e.value);
copyRequireMessage($target, $select);
}
if ($target.find('> label:not([for])').length) {
$target.find('> label').toggleClass('js-form-required form-required', e.value);
}
// 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')) {
$target.find('legend span.fieldset-legend:not(.visually-hidden)').toggleClass('js-form-required form-required', e.value);
}
// 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).trigger('change');
}
});
$document.on('state:readonly', function (e) {
if (e.trigger && $(e.target).isWebformElement()) {
$(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).isWebformElement()) {
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.on('state:visible-slide', function (e) {
if (e.trigger && $(e.target).isWebformElement()) {
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).isWebformElement()) {
// 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');
}
});
/* ************************************************************************ */
// Behaviors.
/* ************************************************************************ */
/**
* Adds HTML5 validation to required checkboxes.
*
* @type {Drupal~behavior}
*
* @see https://www.drupal.org/project/webform/issues/3068998
*/
Drupal.behaviors.webformCheckboxesRequired = {
attach: function (context) {
$('.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)
.once('webform-checkboxes-required')
.each(function () {
var $element = $(this);
$element.find('input[type="checkbox"]').on('click', statesCheckboxesRequiredEventHandler);
setTimeout(function () {checkboxesRequired($element);});
});
}
};
/**
* Adds HTML5 validation to required radios.
*
* @type {Drupal~behavior}
*
* @see https://www.drupal.org/project/webform/issues/2856795
*/
Drupal.behaviors.webformRadiosRequired = {
attach: function (context) {
$('.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)
.once('webform-radios-required')
.each(function () {
var $element = $(this);
setTimeout(function () {radiosRequired($element);});
});
}
};
/**
* Adds HTML5 validation to required table select.
*
* @type {Drupal~behavior}
*
* @see https://www.drupal.org/project/webform/issues/2856795
*/
Drupal.behaviors.webformTableSelectRequired = {
attach: function (context) {
$('.js-webform-tableselect.required', context)
.once('webform-tableselect-required')
.each(function () {
var $element = $(this);
var $tbody = $element.find('tbody');
var isMultiple = $element.is('[multiple]');
if (isMultiple) {
// Check all checkbox triggers checkbox 'change' event on
// select and deselect all.
// @see Drupal.tableSelect
$tbody.find('input[type="checkbox"]').on('click change', function () {
checkboxesRequired($tbody);
});
}
setTimeout(function () {
isMultiple ? checkboxesRequired($tbody) : radiosRequired($element);
});
});
}
};
/**
* Add HTML5 multiple checkboxes required validation.
*
* @param {jQuery} $element
* An jQuery object containing HTML5 radios.
*
* @see https://stackoverflow.com/a/37825072/145846
*/
function checkboxesRequired($element) {
var $firstCheckbox = $element.find('input[type="checkbox"]').first();
var isChecked = $element.find('input[type="checkbox"]').is(':checked');
toggleRequired($firstCheckbox, !isChecked);
copyRequireMessage($element, $firstCheckbox);
}
/**
* Add HTML5 radios required validation.
*
* @param {jQuery} $element
* An jQuery object containing HTML5 radios.
*
* @see https://www.drupal.org/project/webform/issues/2856795
*/
function radiosRequired($element) {
var $radios = $element.find('input[type="radio"]');
var isRequired = $element.hasClass('required');
toggleRequired($radios, isRequired);
copyRequireMessage($element, $radios);
}
/* ************************************************************************ */
// Event handlers.
/* ************************************************************************ */
/**
* Trigger #states API HTML5 multiple checkboxes required validation.
*
* @see https://stackoverflow.com/a/37825072/145846
*/
function statesCheckboxesRequiredEventHandler() {
var $element = $(this).closest('.js-webform-type-checkboxes, .js-webform-type-webform-checkboxes-other');
checkboxesRequired($element);
}
/**
* 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') {
// Do not trigger the onchange event for Address element's country code
// when it is initialized.
// @see \Drupal\address\Element\Country
if ($input.closest('.webform-type-address').length) {
if (!$input.data('webform-states-address-initialized')
&& $input.attr('autocomplete') === 'country'
&& $input.val() === $input.find("option[selected]").attr('value')) {
return;
}
$input.data('webform-states-address-initialized', true);
}
$input
.trigger('change', extraParameters)
.trigger('blur', extraParameters);
}
else if (type !== 'submit' && type !== 'button' && type !== 'file') {
// Make sure input mask is removed and then reset when value is restored.
// @see https://www.drupal.org/project/webform/issues/3124155
// @see https://www.drupal.org/project/webform/issues/3202795
var hasInputMask = ($.fn.inputmask && $input.hasClass('js-webform-input-mask'));
hasInputMask && $input.inputmask('remove');
$input
.trigger('input', extraParameters)
.trigger('change', extraParameters)
.trigger('keydown', extraParameters)
.trigger('keyup', extraParameters)
.trigger('blur', extraParameters);
hasInputMask && $input.inputmask();
}
}
/* ************************************************************************ */
// Backup and restore value functions.
/* ************************************************************************ */
/**
* 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) {
// Prevent "Syntax error, unrecognized expression" error by
// escaping single quotes.
// @see https://forum.jquery.com/topic/escape-characters-prior-to-using-selector
option_value = option_value.replace(/'/g, "\\\'");
$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);
}
/* ************************************************************************ */
// Helper functions.
/* ************************************************************************ */
/**
* Toggle an input's required attributes.
*
* @param {element} $input
* An input.
* @param {boolean} required
* Is input required.
*/
function toggleRequired($input, required) {
var isCheckboxOrRadio = ($input.attr('type') === 'radio' || $input.attr('type') === 'checkbox');
if (required) {
if (isCheckboxOrRadio) {
$input.attr({'required': 'required'});
}
else {
$input.attr({'required': 'required', 'aria-required': 'true'});
}
}
else {
if (isCheckboxOrRadio) {
$input.removeAttr('required');
}
else {
$input.removeAttr('required aria-required');
}
}
}
/**
* Copy the clientside_validation.module's message.
*
* @param {jQuery} $source
* The source element.
* @param {jQuery} $destination
* The destination element.
*/
function copyRequireMessage($source, $destination) {
if ($source.attr('data-msg-required')) {
$destination.attr('data-msg-required', $source.attr('data-msg-required'));
}
}
})(jQuery, Drupal);