first commit

This commit is contained in:
phil 2023-09-30 09:40:37 +02:00
commit 346d1cb29d
287 changed files with 43533 additions and 0 deletions

View file

@ -0,0 +1,23 @@
.antibot-message {
border: 1px solid;
border-width: 1px 1px 1px 0;
border-radius: 2px;
padding: 15px;
word-wrap: break-word;
overflow-wrap: break-word;
margin: 9px 0 10px 8px;
}
.antibot-message-warning {
background-color: #fdf8ed;
border-color: #f4daa6 #f4daa6 #f4daa6 transparent;
color: #734c00;
box-shadow: -8px 0 0 #e09600;
}
.antibot-message-error {
background-color: #fcf4f2;
color: #a51b00;
border-color: #f9c9bf #f9c9bf #f9c9bf transparent;
box-shadow: -8px 0 0 #e62600;
}

View file

@ -0,0 +1,70 @@
/**
* @file
* Unlock protected forms.
*
* This works by resetting the form action to the path that It should be as well
* as injecting the secret form key, only if the current user is verified to be
* human which is done by waiting for a mousemove, swipe, or tab/enter key to be
* pressed.
*/
(function (Drupal, drupalSettings) {
"use strict";
Drupal.antibot = {};
Drupal.behaviors.antibot = {
attach: function (context) {
// Assume the user is not human, despite JS being enabled.
drupalSettings.antibot.human = false;
// Wait for a mouse to move, indicating they are human.
document.body.addEventListener('mousemove', function () {
// Unlock the forms.
Drupal.antibot.unlockForms();
});
// Wait for a touch move event, indicating that they are human.
document.body.addEventListener('touchmove', function () {
// Unlock the forms.
Drupal.antibot.unlockForms();
});
// A tab or enter key pressed can also indicate they are human.
document.body.addEventListener('keydown', function (e) {
if ((e.code == 'Tab') || (e.code == 'Enter')) {
// Unlock the forms.
Drupal.antibot.unlockForms();
}
});
}
};
/**
* Unlock all locked forms.
*/
Drupal.antibot.unlockForms = function () {
// Act only if we haven't yet verified this user as being human.
if (!drupalSettings.antibot.human) {
// Check if there are forms to unlock.
if (drupalSettings.antibot.forms != undefined) {
// Iterate all antibot forms that we need to unlock.
Object.values(drupalSettings.antibot.forms).forEach(function (config) {
// Switch the action.
const form = document.getElementById(config.id);
if (form) {
form.setAttribute('action', form.getAttribute('data-action'));
// Set the key.
const input = form.querySelector('input[name="antibot_key"]');
if (input) {
input.value = config.key;
}
}
});
}
// Mark this user as being human.
drupalSettings.antibot.human = true;
}
};
})(Drupal, drupalSettings);

View file

@ -0,0 +1,51 @@
/**
* @file
* Attaches several event listener to a web page.
*/
(function ($, drupalSettings) {
'use strict';
$(document).ready(function () {
defaultBind();
// Colorbox: This event triggers when the transition has completed and the
// newly loaded content has been revealed.
if (drupalSettings.matomo && drupalSettings.matomo.trackColorbox) {
$(document).bind('cbox_complete', function () {
var href = $.colorbox.element().attr('href');
if (href) {
_paq.push(['setCustomUrl', href]);
if (drupalSettings.matomo.disableCookies) {
_paq.push(['disableCookies']);
}
_paq.push(['trackPageView']);
}
});
}
});
/**
* Default event binding.
*
* Attach mousedown, keyup, touchstart events to document only and catch
* clicks on all elements.
*/
function defaultBind() {
$(document.body).bind('mousedown keyup touchstart', function (event) {
// Catch the closest surrounding link of a clicked element.
$(event.target).closest('a,area').each(function () {
if (drupalSettings.matomo.trackMailto && $(this).is("a[href^='mailto:'],area[href^='mailto:']")) {
// Mailto link clicked.
_paq.push(['trackEvent', 'Mails', 'Click', this.href.substring(7)]);
}
});
});
}
})(jQuery, drupalSettings);

View file

@ -0,0 +1,56 @@
/**
* @file
* Ajax styles.
*/
/**
* Make sure full screen progress indicator is in front of .ui-dialog.
*
* @see core/themes/seven/css/components/dialog.css
*/
.ajax-progress.ajax-progress-fullscreen {
z-index: 1261;
}
/**
* Floating Ajax message container.
*
* Display status message in a floating container at the bottom of the page.
* NOTE: It is display to display message floating at top because of the floating
* admin toolbar.
*
* @see Drupal.AjaxCommands.prototype.webformInsert
*/
.webform-ajax-messages {
position: fixed;
z-index: 100;
bottom: 0;
width: 100%;
}
.webform-ajax-messages .messages {
margin: 0;
border-width: 10px 0 0 0;
font-weight: bold;
}
.webform-ajax-messages .messages + .messages {
margin: 0;
}
/**
* Always position webform modal dialog at the top of the page.
*
* This prevents the dialogs position from jumping as its content is refreshed
* and when the window is resized.
*
* @see core/misc/dialog/dialog.position.js
* @see \Drupal\webform\Utility\WebformDialogHelper::getModalDialogAttributes
*/
.webform-ui-dialog {
top: 50px !important;
}
.toolbar-tray-open.toolbar-horizontal .webform-ui-dialog {
top: 90px !important;
}

View file

@ -0,0 +1,47 @@
/**
* @file
* Details toggle element styles.
*
* @see /webform/test_form_details_toggle
*/
.webform-details-toggle-state-wrapper {
text-align: right; /* LTR */
margin-top: 1em;
}
[dir="rtl"] .webform-details-toggle-state-wrapper {
text-align: left;
}
.webform-details-toggle-state-wrapper + details {
margin-top: 0;
}
/* Tweak details toggle state. */
.webform-details-toggle-state {
margin-top: 0;
padding: 0;
cursor: pointer;
border: 0;
background: transparent;
font-size: 1em;
text-decoration: none;
color: #337ab7;
}
.webform-details-toggle-state:hover,
.webform-details-toggle-statelink:focus {
text-decoration: underline;
}
/* Float toggle to the right of webform tabs */
.webform-tabs .webform-details-toggle-state-wrapper {
float: right;
}
@media screen and (max-width: 600px) {
.webform-tabs .webform-details-toggle-state-wrapper {
float: none;
}
}

View file

@ -0,0 +1,63 @@
/**
* @file
* Message element styles.
*
* @see /webform/test_element_message
*/
/**
* Webform message close container.
*/
.webform-message--close .messages {
position: relative;
padding-right: 35px;
}
.webform-message--close .webform-message__link {
display: none;
}
html.js .webform-message--close .webform-message__link {
position: absolute;
top: 11px;
right: 10px;
display: block;
font-size: 24px;
line-height: 24px;
}
html[dir="rtl"].js .webform-message--close .webform-message__link {
top: 11px;
right: inherit;
left: 10px;
}
.webform-message__link {
opacity: 0.33;
color: inherit;
}
.webform-message__link:link {
text-decoration: none;
border-bottom: none;
}
.webform-message__link:hover,
.webform-message__link:focus,
.webform-message__link:active {
text-decoration: none;
opacity: 1;
color: inherit;
border-bottom: none;
}
html.js .js-webform-message--close-storage {
display: none;
}
/**
* Hide Gin themes dismiss button even when it appears on the node edit form.
*/
.js-webform-message--close .button--dismiss {
display: none;
}

View file

@ -0,0 +1,131 @@
/**
* @file
* Webform form styles.
*/
/**
* This allows components to be hidden when a JS plugin provides the UI.
*/
html.js .js-webform-visually-hidden,
html.js .js-webform-visually-hidden[style*="display: none"] {
position: absolute !important;
display: inline !important;
overflow: hidden;
clip: rect(1px, 1px, 1px, 1px);
width: 1px;
height: 1px;
word-wrap: normal;
}
/**
* Element states.
* @see \Drupal\webform\WebformSubmissionConditionsValidator::buildForm
* @see \Drupal\webform\Utility\WebformElementHelper::fixStatesWrapper
* @see \Drupal\webform\Plugin\WebformElement\TextFormat::preRenderFixTextFormatStates
* @see text-format-wrapper.html.twig
*/
.js-form-item.js-webform-states-hidden,
.js-form-submit.js-webform-states-hidden,
.js-form-wrapper.js-webform-states-hidden,
.js-webform-text-format-hidden > .js-text-format-wrapper {
display: none;
}
/**
* Form inline. (This is not included in all themes)
*/
.form--inline .form-item {
float: left; /* LTR */
margin-right: 0.5em; /* LTR */
}
[dir="rtl"] .form--inline .form-item {
float: right;
margin-right: 0;
margin-left: 0.5em;
}
/**
* Container inline
*/
.form-item .container-inline {
margin: 2px 0;
}
/**
* Issue #2731991: Setting required on radios marks all options required.
*/
.form-checkboxes .form-required:after,
.form-radios .form-required:after {
display: none;
}
/**
* Element title inline.z
*/
.webform-element--title-inline > label {
display: inline;
padding-right: 0.5em;
}
.webform-element--title-inline > div.container-inline {
display: inline;
}
/**
* Fieldset title inline.
* Applies to radios, checkboxes, and buttons. (aka .form-composite)
* @see \Drupal\webform\Plugin\WebformElement\OptionsBase::prepare
* @see webform_preprocess_fieldset()
*/
.form-composite.webform-fieldset--title-inline legend {
float: left; /* LTR */
margin: 0.4em 0.5em 0.4em 0; /* LTR */
}
[dir=rtl] .form-composite.webform-fieldset--title-inline legend {
float: right; /* RTL */
margin-right: 0; /* RTL */
margin-left: 0.5em; /* RTL */
}
.form-composite.webform-fieldset--title-inline .fieldset-wrapper,
.form-composite.webform-fieldset--title-inline .fieldset-wrapper > div {
display: inline;
}
/**
* Checkboxes and radios title inline.
*/
.webform-element--title-inline .form-radios,
.webform-element--title-inline .form-checkboxes {
display: inline;
}
/**
* Clientside validation errors.
* @see webform_clientside_validation.ife.css
*/
.webform-submission-form strong.error.form-item--error-message {
display: block;
}
/**
* Readonly inputs. (@see .form-disabled)
* @see https://www.wufoo.com/html5/attributes/21-readonly.html
*/
.webform-readonly input[type="date"],
.webform-readonly input[type="datetime-local"],
.webform-readonly input[type="email"],
.webform-readonly input[type="number"],
.webform-readonly input[type="password"],
.webform-readonly input[type="search"],
.webform-readonly input[type="tel"],
.webform-readonly input[type="text"],
.webform-readonly input[type="time"],
.webform-readonly input[type="url"],
.webform-readonly textarea {
color: #717171;
border-color: #bbb;
background: #ededed;
}

View file

@ -0,0 +1,25 @@
/**
* @file
* Classy theme styles.
*/
/**
* Make sure the date picker is in front of the dialog.
*
* @see core/themes/classy/css/components/dialog.css
* @see core/themes/seven/css/components/dialog.css
*/
.ui-datepicker {
z-index: 1261 !important;
}
/**
* Ajax form wrapper.
*
* Prevent actions bottom margin from causing the slide effect to
* jump when completed.
*/
.webform-ajax-form-wrapper[data-effect="slide"] .form-actions {
margin-bottom: 0;
padding-bottom: 1em;
}

View file

@ -0,0 +1,337 @@
/**
* @file
* JavaScript behaviors for Ajax.
*/
(function ($, Drupal, drupalSettings) {
'use strict';
Drupal.webform = Drupal.webform || {};
Drupal.webform.ajax = Drupal.webform.ajax || {};
// Allow scrollTopOffset to be custom defined or based on whether there is a
// floating toolbar.
Drupal.webform.ajax.scrollTopOffset = Drupal.webform.ajax.scrollTopOffset || ($('#toolbar-administration').length ? 140 : 10);
// Set global scroll top offset.
// @todo Remove in Webform 6.x.
Drupal.webform.scrollTopOffset = Drupal.webform.ajax.scrollTopOffset;
/**
* Provide Webform Ajax link behavior.
*
* Display fullscreen progress indicator instead of throbber.
* Copied from: Drupal.behaviors.AJAX
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the behavior to a.webform-ajax-link.
*/
Drupal.behaviors.webformAjaxLink = {
attach: function (context) {
$('.webform-ajax-link', context).once('webform-ajax-link').each(function () {
var element_settings = {};
element_settings.progress = {type: 'fullscreen'};
// For anchor tags, these will go to the target of the anchor rather
// than the usual location.
var href = $(this).attr('href');
if (href) {
element_settings.url = href;
element_settings.event = 'click';
}
element_settings.dialogType = $(this).data('dialog-type');
element_settings.dialogRenderer = $(this).data('dialog-renderer');
element_settings.dialog = $(this).data('dialog-options');
element_settings.base = $(this).attr('id');
element_settings.element = this;
Drupal.ajax(element_settings);
// Close all open modal dialogs when opening off-canvas dialog.
if (element_settings.dialogRenderer === 'off_canvas') {
$(this).on('click', function () {
$('.ui-dialog.webform-ui-dialog:visible').find('.ui-dialog-content').dialog('close');
});
}
});
}
};
/**
* Adds a hash (#) to current pages location for links and buttons
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the behavior to a[data-hash] or :button[data-hash].
*
* @see \Drupal\webform_ui\WebformUiEntityElementsForm::getElementRow
* @see Drupal.behaviors.webformFormTabs
*/
Drupal.behaviors.webformAjaxHash = {
attach: function (context) {
$('[data-hash]', context).once('webform-ajax-hash').each(function () {
var hash = $(this).data('hash');
if (hash) {
$(this).on('click', function () {
location.hash = $(this).data('hash');
});
}
});
}
};
/**
* Provide Ajax callback for confirmation back to link.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the behavior to confirmation back to link.
*/
Drupal.behaviors.webformConfirmationBackAjax = {
attach: function (context) {
$('.js-webform-confirmation-back-link-ajax', context)
.once('webform-confirmation-back-ajax')
.on('click', function (event) {
var $form = $(this).parents('form');
// Trigger the Ajax call back for the hidden submit button.
// @see \Drupal\webform\WebformSubmissionForm::getCustomForm
$form.find('.js-webform-confirmation-back-submit-ajax').trigger('click');
// Move the progress indicator from the submit button to after this link.
// @todo Figure out a better way to set a progress indicator.
var $progress_indicator = $form.find('.ajax-progress');
if ($progress_indicator) {
$(this).after($progress_indicator);
}
// Cancel the click event.
event.preventDefault();
event.stopPropagation();
});
}
};
/** ********************************************************************** **/
// Ajax commands.
/** ********************************************************************** **/
/**
* Track the updated table row key.
*/
var updateKey;
/**
* Track the add element key.
*/
var addElement;
/**
* Command to insert new content into the DOM.
*
* @param {Drupal.Ajax} ajax
* {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
* @param {object} response
* The response from the Ajax request.
* @param {string} response.data
* The data to use with the jQuery method.
* @param {string} [response.method]
* The jQuery DOM manipulation method to be used.
* @param {string} [response.selector]
* A optional jQuery selector string.
* @param {object} [response.settings]
* An optional array of settings that will be used.
* @param {number} [status]
* The XMLHttpRequest status.
*/
Drupal.AjaxCommands.prototype.webformInsert = function (ajax, response, status) {
// Insert the HTML.
this.insert(ajax, response, status);
// Add element.
if (addElement) {
var addSelector = (addElement === '_root_')
? '#webform-ui-add-element'
: '[data-drupal-selector="edit-webform-ui-elements-' + addElement + '-add"]';
$(addSelector).trigger('click');
}
// If not add element, then scroll to and highlight the updated table row.
if (!addElement && updateKey) {
var $element = $('tr[data-webform-key="' + updateKey + '"]');
// Highlight the updated element's row.
$element.addClass('color-success');
setTimeout(function () {$element.removeClass('color-success');}, 3000);
// Focus first tabbable item for the updated elements and handlers.
$element.find(':tabbable:not(.tabledrag-handle)').eq(0).trigger('focus');
// Scroll element into view.
Drupal.webformScrolledIntoView($element);
}
else {
// Focus main content.
$('#main-content').trigger('focus');
}
// Display main page's status message in a floating container.
var $wrapper = $(response.selector);
if ($wrapper.parents('.ui-dialog').length === 0) {
var $messages = $wrapper.find('.messages');
// If 'add element' don't show any messages.
if (addElement) {
$messages.remove();
}
else if ($messages.length) {
var $floatingMessage = $('#webform-ajax-messages');
if ($floatingMessage.length === 0) {
$floatingMessage = $('<div id="webform-ajax-messages" class="webform-ajax-messages"></div>');
$('body').append($floatingMessage);
}
if ($floatingMessage.is(':animated')) {
$floatingMessage.stop(true, true);
}
$floatingMessage.html($messages).show().delay(3000).fadeOut(1000);
}
}
updateKey = null; // Reset element update.
addElement = null; // Reset add element.
};
/**
* Scroll to top ajax command.
*
* @param {Drupal.Ajax} [ajax]
* A {@link Drupal.ajax} object.
* @param {object} response
* Ajax response.
* @param {string} response.selector
* Selector to use.
*
* @see Drupal.AjaxCommands.prototype.viewScrollTop
*/
Drupal.AjaxCommands.prototype.webformScrollTop = function (ajax, response) {
// Scroll top.
Drupal.webformScrollTop(response.selector, response.target);
// Focus on the form wrapper content bookmark if
// .js-webform-autofocus is not enabled.
// @see \Drupal\webform\Form\WebformAjaxFormTrait::buildAjaxForm
var $form = $(response.selector + '-content').find('form');
if (!$form.hasClass('js-webform-autofocus')) {
$(response.selector + '-content').trigger('focus');
}
};
/**
* Command to refresh the current webform page.
*
* @param {Drupal.Ajax} [ajax]
* {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
* @param {object} response
* The response from the Ajax request.
* @param {string} response.url
* The URL to redirect to.
* @param {number} [status]
* The XMLHttpRequest status.
*/
Drupal.AjaxCommands.prototype.webformRefresh = function (ajax, response, status) {
// Get URL path name.
// @see https://stackoverflow.com/questions/6944744/javascript-get-portion-of-url-path
var a = document.createElement('a');
a.href = response.url;
var forceReload = (response.url.match(/\?reload=([^&]+)($|&)/)) ? RegExp.$1 : null;
if (forceReload) {
response.url = response.url.replace(/\?reload=([^&]+)($|&)/, '');
this.redirect(ajax, response, status);
return;
}
if (a.pathname === window.location.pathname && $('.webform-ajax-refresh').length) {
updateKey = (response.url.match(/[?|&]update=([^&]+)($|&)/)) ? RegExp.$1 : null;
addElement = (response.url.match(/[?|&]add_element=([^&]+)($|&)/)) ? RegExp.$1 : null;
$('.webform-ajax-refresh').trigger('click');
}
else {
// Clear unsaved information flag so that the current webform page
// can be redirected.
// @see Drupal.behaviors.webformUnsaved.clear
if (Drupal.behaviors.webformUnsaved) {
Drupal.behaviors.webformUnsaved.clear();
}
// For webform embedded in an iframe, open all redirects in the top
// of the browser window.
// @see \Drupal\webform_share\Controller\WebformShareController::page
if (drupalSettings.webform_share &&
drupalSettings.webform_share.page) {
window.top.location = response.url;
}
else {
this.redirect(ajax, response, status);
}
}
};
/**
* Command to close a off-canvas and modal dialog.
*
* If no selector is given, it defaults to trying to close the modal.
*
* @param {Drupal.Ajax} [ajax]
* {@link Drupal.Ajax} object created by {@link Drupal.ajax}.
* @param {object} response
* The response from the Ajax request.
* @param {string} response.selector
* Selector to use.
* @param {bool} response.persist
* Whether to persist the dialog element or not.
* @param {number} [status]
* The HTTP status code.
*/
Drupal.AjaxCommands.prototype.webformCloseDialog = function (ajax, response, status) {
if ($('#drupal-off-canvas').length) {
// Close off-canvas system tray which is not triggered by close dialog
// command.
// @see Drupal.behaviors.offCanvasEvents
$('#drupal-off-canvas').remove();
$('body').removeClass('js-tray-open');
// Remove all *.off-canvas events
$(document).off('.off-canvas');
$(window).off('.off-canvas');
var edge = document.documentElement.dir === 'rtl' ? 'left' : 'right';
var $mainCanvasWrapper = $('[data-off-canvas-main-canvas]');
$mainCanvasWrapper.css('padding-' + edge, 0);
// Resize tabs when closing off-canvas system tray.
$(window).trigger('resize.tabs');
}
// https://stackoverflow.com/questions/15763909/jquery-ui-dialog-check-if-exists-by-instance-method
if ($(response.selector).hasClass('ui-dialog-content')) {
this.closeDialog(ajax, response, status);
}
};
/**
* Triggers confirm page reload.
*
* @param {Drupal.Ajax} [ajax]
* A {@link Drupal.ajax} object.
* @param {object} response
* Ajax response.
* @param {string} response.message
* A message to be displayed in the confirm dialog.
*/
Drupal.AjaxCommands.prototype.webformConfirmReload = function (ajax, response) {
if (window.confirm(response.message)) {
window.location.reload(true);
}
};
})(jQuery, Drupal, drupalSettings);

View file

@ -0,0 +1,41 @@
/**
* @file
* Webform behaviors.
*/
(function ($, Drupal) {
'use strict';
// Trigger Drupal's attaching of behaviors after the page is
// completely loaded.
// @see https://stackoverflow.com/questions/37838430/detect-if-page-is-load-from-back-button
// @see https://stackoverflow.com/questions/20899274/how-to-refresh-page-on-back-button-click/20899422#20899422
var isChrome = (/chrom(e|ium)/.test(window.navigator.userAgent.toLowerCase()));
if (isChrome) {
// Track back button in navigation.
// @see https://stackoverflow.com/questions/37838430/detect-if-page-is-load-from-back-button
var backButton = false;
if (window.performance) {
var navEntries = window.performance.getEntriesByType('navigation');
if (navEntries.length > 0 && navEntries[0].type === 'back_forward') {
backButton = true;
}
else if (window.performance.navigation
&& window.performance.navigation.type === window.performance.navigation.TYPE_BACK_FORWARD) {
backButton = true;
}
}
// If the back button is pressed, delay Drupal's attaching of behaviors.
if (backButton) {
var attachBehaviors = Drupal.attachBehaviors;
Drupal.attachBehaviors = function (context, settings) {
setTimeout(function (context, settings) {
attachBehaviors(context, settings);
}, 300);
};
}
}
})(jQuery, Drupal);

View file

@ -0,0 +1,126 @@
/**
* @file
* JavaScript behaviors for details element.
*/
(function ($, Drupal) {
'use strict';
// Determine if local storage exists and is enabled.
// This approach is copied from Modernizr.
// @see https://github.com/Modernizr/Modernizr/blob/c56fb8b09515f629806ca44742932902ac145302/modernizr.js#L696-731
var hasLocalStorage = (function () {
try {
localStorage.setItem('webform', 'webform');
localStorage.removeItem('webform');
return true;
}
catch (e) {
return false;
}
}());
/**
* Attach handler to save details open/close state.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformDetailsSave = {
attach: function (context) {
if (!hasLocalStorage) {
return;
}
// Summary click event handler.
$('details > summary', context).once('webform-details-summary-save').on('click', function () {
var $details = $(this).parent();
// @see https://css-tricks.com/snippets/jquery/make-an-jquery-hasattr/
if ($details[0].hasAttribute('data-webform-details-nosave')) {
return;
}
var name = Drupal.webformDetailsSaveGetName($details);
if (!name) {
return;
}
var open = ($details.attr('open') !== 'open') ? '1' : '0';
localStorage.setItem(name, open);
});
// Initialize details open state via local storage.
$('details', context).once('webform-details-save').each(function () {
var $details = $(this);
var name = Drupal.webformDetailsSaveGetName($details);
if (!name) {
return;
}
var open = localStorage.getItem(name);
if (open === null) {
return;
}
if (open === '1') {
$details.attr('open', 'open');
}
else {
$details.removeAttr('open');
}
});
}
};
/**
* Get the name used to store the state of details element.
*
* @param {jQuery} $details
* A details element.
*
* @return {string}
* The name used to store the state of details element.
*/
Drupal.webformDetailsSaveGetName = function ($details) {
if (!hasLocalStorage) {
return '';
}
// Ignore details that are vertical tabs pane.
if ($details.hasClass('vertical-tabs__pane')) {
return '';
}
// Any details element not included a webform must have define its own id.
var webformId = $details.attr('data-webform-element-id');
if (webformId) {
return 'Drupal.webform.' + webformId.replace('--', '.');
}
var detailsId = $details.attr('id');
if (!detailsId) {
return '';
}
var $form = $details.parents('form');
if (!$form.length || !$form.attr('id')) {
return '';
}
var formId = $form.attr('id');
if (!formId) {
return '';
}
// ISSUE: When Drupal renders a webform in a modal dialog it appends a unique
// identifier to webform ids and details ids. (i.e. my-form--FeSFISegTUI)
// WORKAROUND: Remove the unique id that delimited using double dashes.
formId = formId.replace(/--.+?$/, '').replace(/-/g, '_');
detailsId = detailsId.replace(/--.+?$/, '').replace(/-/g, '_');
return 'Drupal.webform.' + formId + '.' + detailsId;
};
})(jQuery, Drupal);

View file

@ -0,0 +1,118 @@
/**
* @file
* JavaScript behaviors for details element.
*/
(function ($, Drupal) {
'use strict';
Drupal.webform = Drupal.webform || {};
Drupal.webform.detailsToggle = Drupal.webform.detailsToggle || {};
Drupal.webform.detailsToggle.options = Drupal.webform.detailsToggle.options || {};
/**
* Attach handler to toggle details open/close state.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformDetailsToggle = {
attach: function (context) {
$('.js-webform-details-toggle', context).once('webform-details-toggle').each(function () {
var $form = $(this);
var $tabs = $form.find('.webform-tabs');
// Get only the main details elements and ignore all nested details.
var selector = ($tabs.length) ? '.webform-tab' : '.js-webform-details-toggle, .webform-elements';
var $details = $form.find('details').filter(function () {
var $parents = $(this).parentsUntil(selector);
return ($parents.find('details').length === 0);
});
// Toggle is only useful when there are two or more details elements.
if ($details.length < 2) {
return;
}
var options = $.extend({
button: '<button type="button" class="webform-details-toggle-state"></button>'
}, Drupal.webform.detailsToggle.options);
// Create toggle buttons.
var $toggle = $(options.button)
.attr('title', Drupal.t('Toggle details widget state.'))
.on('click', function (e) {
// Get details that are not vertical tabs pane.
var $details = $form.find('details:not(.vertical-tabs__pane)');
var open;
if (Drupal.webform.detailsToggle.isFormDetailsOpen($form)) {
$details.removeAttr('open');
open = 0;
}
else {
$details.attr('open', 'open');
open = 1;
}
Drupal.webform.detailsToggle.setDetailsToggleLabel($form);
// Set the saved states for all the details elements.
// @see webform.element.details.save.js
if (Drupal.webformDetailsSaveGetName) {
$details.each(function () {
// Note: Drupal.webformDetailsSaveGetName checks if localStorage
// exists and is enabled.
// @see webform.element.details.save.js
var name = Drupal.webformDetailsSaveGetName($(this));
if (name) {
localStorage.setItem(name, open);
}
});
}
})
.wrap('<div class="webform-details-toggle-state-wrapper"></div>')
.parent();
if ($tabs.length) {
// Add toggle state before the tabs.
$tabs.find('.item-list:first-child').eq(0).before($toggle);
}
else {
// Add toggle state link to first details element.
$details.eq(0).before($toggle);
}
Drupal.webform.detailsToggle.setDetailsToggleLabel($form);
});
}
};
/**
* Determine if a webform's details are all opened.
*
* @param {jQuery} $form
* A webform.
*
* @return {boolean}
* TRUE if a webform's details are all opened.
*/
Drupal.webform.detailsToggle.isFormDetailsOpen = function ($form) {
return ($form.find('details[open]').length === $form.find('details').length);
};
/**
* Set a webform's details toggle state widget label.
*
* @param {jQuery} $form
* A webform.
*/
Drupal.webform.detailsToggle.setDetailsToggleLabel = function ($form) {
var isOpen = Drupal.webform.detailsToggle.isFormDetailsOpen($form);
var label = (isOpen) ? Drupal.t('Collapse all') : Drupal.t('Expand all');
$form.find('.webform-details-toggle-state').html(label);
var text = (isOpen) ? Drupal.t('All details have been expanded.') : Drupal.t('All details have been collapsed.');
Drupal.announce(text);
};
})(jQuery, Drupal);

View file

@ -0,0 +1,28 @@
/**
* @file
* JavaScript behaviors for details element.
*/
(function ($, Drupal) {
'use strict';
/**
* Attach handler to details with invalid inputs.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformDetailsInvalid = {
attach: function (context) {
$('details :input', context).on('invalid', function () {
$(this).parents('details:not([open])').children('summary').trigger('click');
// Synd details toggle label.
if (Drupal.webform && Drupal.webform.detailsToggle) {
Drupal.webform.detailsToggle.setDetailsToggleLabel($(this.form));
}
});
}
};
})(jQuery, Drupal);

View file

@ -0,0 +1,127 @@
/**
* @file
* JavaScript behaviors for message element integration.
*/
(function ($, Drupal) {
'use strict';
// Determine if local storage exists and is enabled.
// This approach is copied from Modernizr.
// @see https://github.com/Modernizr/Modernizr/blob/c56fb8b09515f629806ca44742932902ac145302/modernizr.js#L696-731
var hasLocalStorage = (function () {
try {
localStorage.setItem('webform', 'webform');
localStorage.removeItem('webform');
return true;
}
catch (e) {
return false;
}
}());
// Determine if session storage exists and is enabled.
// This approach is copied from Modernizr.
// @see https://github.com/Modernizr/Modernizr/blob/c56fb8b09515f629806ca44742932902ac145302/modernizr.js#L696-731
var hasSessionStorage = (function () {
try {
sessionStorage.setItem('webform', 'webform');
sessionStorage.removeItem('webform');
return true;
}
catch (e) {
return false;
}
}());
/**
* Behavior for handler message close.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformMessageClose = {
attach: function (context) {
$(context).find('.js-webform-message--close').once('webform-message--close').each(function () {
var $element = $(this);
var id = $element.attr('data-message-id');
var storage = $element.attr('data-message-storage');
var effect = $element.attr('data-message-close-effect') || 'hide';
switch (effect) {
case 'slide': effect = 'slideUp'; break;
case 'fade': effect = 'fadeOut'; break;
}
// Check storage status.
if (isClosed($element, storage, id)) {
return;
}
// Only show element if it's style is not set to 'display: none'
// and it is not hidden via .js-webform-states-hidden.
if ($element.attr('style') !== 'display: none;' && !$element.hasClass('js-webform-states-hidden')) {
$element.show();
}
$element.find('.js-webform-message__link').on('click', function (event) {
$element[effect]();
setClosed($element, storage, id);
$element.trigger('close');
event.preventDefault();
});
});
}
};
function isClosed($element, storage, id) {
if (!id || !storage) {
return false;
}
switch (storage) {
case 'local':
if (hasLocalStorage) {
return localStorage.getItem('Drupal.webform.message.' + id) || false;
}
return false;
case 'session':
if (hasSessionStorage) {
return sessionStorage.getItem('Drupal.webform.message.' + id) || false;
}
return false;
default:
return false;
}
}
function setClosed($element, storage, id) {
if (!id || !storage) {
return;
}
switch (storage) {
case 'local':
if (hasLocalStorage) {
localStorage.setItem('Drupal.webform.message.' + id, true);
}
break;
case 'session':
if (hasSessionStorage) {
sessionStorage.setItem('Drupal.webform.message.' + id, true);
}
break;
case 'user':
case 'state':
case 'custom':
$.get($element.find('.js-webform-message__link').attr('href'));
return true;
}
}
})(jQuery, Drupal);

View file

@ -0,0 +1,16 @@
/**
* @file
* JavaScript to disable back button.
*/
(function () {
'use strict';
// From: http://stackoverflow.com/questions/17962130/restrict-user-to-refresh-and-back-forward-in-any-browser
history.pushState({page: 1}, 'Title 1', '#no-back');
window.onhashchange = function (event) {
window.location.hash = 'no-back';
};
})();

View file

@ -0,0 +1,68 @@
/**
* @file
* JavaScript behaviors for preventing duplicate webform submissions.
*/
(function ($, Drupal) {
'use strict';
/**
* Submit once.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the behavior for preventing duplicate webform submissions.
*/
Drupal.behaviors.webformSubmitOnce = {
clear: function () {
var $form = $('.js-webform-submit-once');
$form.removeData('webform-submitted');
$form.find('.js-webform-wizard-pages-links :submit, .form-actions :submit').removeClass('is-disabled');
$form.find('.form-actions .ajax-progress.ajax-progress-throbber').remove();
},
attach: function (context) {
$('.js-webform-submit-once', context).once('webform-submit-once').each(function () {
var $form = $(this);
// Remove data-webform-submitted.
$form.removeData('webform-submitted');
// Remove .js-webform-submit-clicked.
$form.find('.js-webform-wizard-pages-links :submit, .form-actions :submit').removeClass('js-webform-submit-clicked');
// Track which submit button was clicked.
// @see http://stackoverflow.com/questions/5721724/jquery-how-to-get-which-button-was-clicked-upon-form-submission
$form.find('.js-webform-wizard-pages-links :submit, .form-actions :submit').on('click', function () {
$form.find('.js-webform-wizard-pages-links :submit, .form-actions :submit')
.removeClass('js-webform-submit-clicked');
$(this)
.addClass('js-webform-submit-clicked');
});
$(this).on('submit', function () {
// Find clicked button
var $clickedButton = $form.find('.js-webform-wizard-pages-links :submit.js-webform-submit-clicked, .form-actions :submit.js-webform-submit-clicked');
// Don't submit if client-side validation has failed.
if (!$clickedButton.attr('formnovalidate') && $.isFunction(jQuery.fn.valid) && !($form.valid())) {
return false;
}
// Track webform submitted.
if ($form.data('webform-submitted')) {
return false;
}
$form.data('webform-submitted', 'true');
// Visually disable all submit buttons.
// Submit buttons can't disabled because their op(eration) must to be posted back to the server.
$form.find('.js-webform-wizard-pages-links :submit, .form-actions :submit').addClass('is-disabled');
// Set the throbber progress indicator.
$clickedButton.after(Drupal.theme.ajaxProgressThrobber());
});
});
}
};
})(jQuery, Drupal);

View file

@ -0,0 +1,137 @@
/**
* @file
* JavaScript behaviors for unsaved webforms.
*/
(function ($, Drupal) {
'use strict';
var unsaved = false;
/**
* Unsaved changes.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the behavior for unsaved changes.
*/
Drupal.behaviors.webformUnsaved = {
clear: function () {
// Allow Ajax refresh/redirect to clear unsaved flag.
// @see Drupal.AjaxCommands.prototype.webformRefresh
unsaved = false;
},
get: function () {
// Get the current unsaved flag state.
return unsaved;
},
set: function (value) {
// Set the current unsaved flag state.
unsaved = value;
},
attach: function (context) {
// Look for the 'data-webform-unsaved' attribute which indicates that
// a multi-step webform has unsaved data.
// @see \Drupal\webform\WebformSubmissionForm::buildForm
if ($('.js-webform-unsaved[data-webform-unsaved]').once('data-webform-unsaved').length) {
unsaved = true;
}
else {
$('.js-webform-unsaved :input:not(:button, :submit, :reset, [type="hidden"])').once('webform-unsaved').on('change keypress', function (event, param1) {
// Ignore events triggered when #states API is changed,
// which passes 'webform.states' as param1.
// @see webform.states.js ::triggerEventHandlers().
if (param1 !== 'webform.states') {
unsaved = true;
}
});
}
$('.js-webform-unsaved button, .js-webform-unsaved input[type="submit"]', context)
.once('webform-unsaved')
.not('[data-webform-unsaved-ignore]')
.on('click', function (event) {
// For reset button we must confirm unsaved changes before the
// before unload event handler.
if ($(this).hasClass('webform-button--reset') && unsaved) {
if (!window.confirm(Drupal.t('Changes you made may not be saved.') + '\n\n' + Drupal.t('Press OK to leave this page or Cancel to stay.'))) {
return false;
}
}
unsaved = false;
});
// Add submit handler to form.beforeSend.
// Update Drupal.Ajax.prototype.beforeSend only once.
if (typeof Drupal.Ajax !== 'undefined' && typeof Drupal.Ajax.prototype.beforeSubmitWebformUnsavedOriginal === 'undefined') {
Drupal.Ajax.prototype.beforeSubmitWebformUnsavedOriginal = Drupal.Ajax.prototype.beforeSubmit;
Drupal.Ajax.prototype.beforeSubmit = function (form_values, element_settings, options) {
unsaved = false;
return this.beforeSubmitWebformUnsavedOriginal.apply(this, arguments);
};
}
// Track all CKEditor change events.
// @see https://ckeditor.com/old/forums/Support/CKEditor-jQuery-change-event
if (window.CKEDITOR && !CKEDITOR.webformUnsaved) {
CKEDITOR.webformUnsaved = true;
CKEDITOR.on('instanceCreated', function (event) {
event.editor.on('change', function (evt) {
unsaved = true;
});
});
}
}
};
$(window).on('beforeunload', function () {
if (unsaved) {
return true;
}
});
/**
* An experimental shim to partially emulate onBeforeUnload on iOS.
* Part of https://github.com/codedance/jquery.AreYouSure/
*
* Copyright (c) 2012-2014, Chris Dance and PaperCut Software http://www.papercut.com/
* Dual licensed under the MIT or GPL Version 2 licenses.
* http://jquery.org/license
*
* Author: chris.dance@papercut.com
* Date: 19th May 2014
*/
$(function () {
// @see https://stackoverflow.com/questions/58019463/how-to-detect-device-name-in-safari-on-ios-13-while-it-doesnt-show-the-correct
var isIOSorOpera = navigator.userAgent.toLowerCase().match(/iphone|ipad|ipod|opera/)
|| navigator.platform.toLowerCase().match(/iphone|ipad|ipod/)
|| (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
if (!isIOSorOpera) {
return;
}
$('a:not(.use-ajax)').bind('click', function (evt) {
var a = $(evt.target).closest('a');
var href = a.attr('href');
if (typeof href !== 'undefined' && !(href.match(/^#/) || href.trim() === '')) {
if ($(window).triggerHandler('beforeunload')) {
if (!window.confirm(Drupal.t('Changes you made may not be saved.') + '\n\n' + Drupal.t('Press OK to leave this page or Cancel to stay.'))) {
return false;
}
}
var target = a.attr('target');
if (target) {
window.open(href, target);
}
else {
window.location.href = href;
}
return false;
}
});
});
})(jQuery, Drupal);

View file

@ -0,0 +1,104 @@
/**
* @file
* JavaScript behaviors for webforms.
*/
(function ($, Drupal) {
'use strict';
/**
* Remove single submit event listener.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the behavior for removing single submit event listener.
*
* @see Drupal.behaviors.formSingleSubmit
*/
Drupal.behaviors.webformRemoveFormSingleSubmit = {
attach: function attach() {
function onFormSubmit(e) {
var $form = $(e.currentTarget);
$form.removeAttr('data-drupal-form-submit-last');
}
$('body')
.once('webform-single-submit')
.on('submit.singleSubmit', 'form.webform-remove-single-submit', onFormSubmit);
}
};
/**
* Prevent webform autosubmit on wizard pages.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the behavior for disabling webform autosubmit.
* Wizard pages need to be progressed with the Previous or Next buttons,
* not by pressing Enter.
*/
Drupal.behaviors.webformDisableAutoSubmit = {
attach: function (context) {
// Not using context so that inputs loaded via Ajax will have autosubmit
// disabled.
// @see http://stackoverflow.com/questions/11235622/jquery-disable-form-submit-on-enter
$('.js-webform-disable-autosubmit input')
.not(':button, :submit, :reset, :image, :file')
.once('webform-disable-autosubmit')
.on('keyup keypress', function (e) {
if (e.which === 13) {
e.preventDefault();
return false;
}
});
}
};
/**
* Custom required and pattern validation error messages.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the behavior for the webform custom required and pattern
* validation error messages.
*
* @see http://stackoverflow.com/questions/5272433/html5-form-required-attribute-set-custom-validation-message
**/
Drupal.behaviors.webformRequiredError = {
attach: function (context) {
$(context).find(':input[data-webform-required-error], :input[data-webform-pattern-error]').once('webform-required-error')
.on('invalid', function () {
this.setCustomValidity('');
if (this.valid) {
return;
}
if (this.validity.patternMismatch && $(this).attr('data-webform-pattern-error')) {
this.setCustomValidity($(this).attr('data-webform-pattern-error'));
}
else if (this.validity.valueMissing && $(this).attr('data-webform-required-error')) {
this.setCustomValidity($(this).attr('data-webform-required-error'));
}
})
.on('input change', function () {
// Find all related elements by name and reset custom validity.
// This specifically applies to required radios and checkboxes.
var name = $(this).attr('name');
$(this.form).find(':input[name="' + name + '"]').each(function () {
this.setCustomValidity('');
});
});
}
};
// When #state:required is triggered we need to reset the target elements
// custom validity.
$(document).on('state:required', function (e) {
$(e.target).filter('[data-webform-required-error]')
.each(function () {this.setCustomValidity('');});
});
})(jQuery, Drupal);

View file

@ -0,0 +1,92 @@
/**
* @file
* JavaScript behaviors for webform scroll top.
*/
(function ($, Drupal) {
'use strict';
Drupal.webform = Drupal.webform || {};
// Allow scrollTopOffset to be custom defined or based on whether there is a
// floating toolbar.
Drupal.webform.scrollTopOffset = Drupal.webform.scrollTopOffset || ($('#toolbar-administration').length ? 140 : 10);
/**
* Scroll to top ajax command.
*
* @param {Element} element
* The element to scroll to.
* @param {string} target
* Scroll to target. (form or page)
*/
Drupal.webformScrollTop = function (element, target) {
if (!target) {
return;
}
var $element = $(element);
// Scroll to the top of the view. This will allow users
// to browse newly loaded content after e.g. clicking a pager
// link.
var offset = $element.offset();
// We can't guarantee that the scrollable object should be
// the body, as the view could be embedded in something
// more complex such as a modal popup. Recurse up the DOM
// and scroll the first element that has a non-zero top.
var $scrollTarget = $element;
while ($scrollTarget.scrollTop() === 0 && $($scrollTarget).parent()) {
$scrollTarget = $scrollTarget.parent();
}
if (target === 'page' && $scrollTarget.length && $scrollTarget[0].tagName === 'HTML') {
// Scroll to top when scroll target is the entire page.
// @see https://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport
var rect = $($scrollTarget)[0].getBoundingClientRect();
if (!(rect.top >= 0 && rect.left >= 0 && rect.bottom <= $(window).height() && rect.right <= $(window).width())) {
$scrollTarget.animate({scrollTop: 0}, 500);
}
}
else {
// Only scroll upward.
if (offset.top - Drupal.webform.scrollTopOffset < $scrollTarget.scrollTop()) {
$scrollTarget.animate({scrollTop: (offset.top - Drupal.webform.scrollTopOffset)}, 500);
}
}
};
/**
* Scroll element into view.
*
* @param {jQuery} $element
* An element.
*/
Drupal.webformScrolledIntoView = function ($element) {
if (!Drupal.webformIsScrolledIntoView($element)) {
$('html, body').animate({scrollTop: $element.offset().top - Drupal.webform.scrollTopOffset}, 500);
}
};
/**
* Determine if element is visible in the viewport.
*
* @param {Element} element
* An element.
*
* @return {boolean}
* TRUE if element is visible in the viewport.
*
* @see https://stackoverflow.com/questions/487073/check-if-element-is-visible-after-scrolling
*/
Drupal.webformIsScrolledIntoView = function (element) {
var docViewTop = $(window).scrollTop();
var docViewBottom = docViewTop + $(window).height();
var elemTop = $(element).offset().top;
var elemBottom = elemTop + $(element).height();
return ((elemBottom <= docViewBottom) && (elemTop >= docViewTop));
};
})(jQuery, Drupal);

View file

@ -0,0 +1,647 @@
/**
* @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);