/** * @file * JavaScript behaviors for the front-end display of webforms. */ (function ($) { "use strict"; Drupal.behaviors.webform = Drupal.behaviors.webform || {}; Drupal.behaviors.webform.attach = function (context) { // Calendar datepicker behavior. Drupal.webform.datepicker(context); // Conditional logic. if (Drupal.settings.webform && Drupal.settings.webform.conditionals) { Drupal.webform.conditional(context); } }; Drupal.webform = Drupal.webform || {}; Drupal.webform.datepicker = function (context) { $('div.webform-datepicker').each(function () { var $webformDatepicker = $(this); var $calendar = $webformDatepicker.find('input.webform-calendar'); // Ensure the page we're on actually contains a datepicker. if ($calendar.length == 0) { return; } var startDate = $calendar[0].className.replace(/.*webform-calendar-start-(\d{4}-\d{2}-\d{2}).*/, '$1').split('-'); var endDate = $calendar[0].className.replace(/.*webform-calendar-end-(\d{4}-\d{2}-\d{2}).*/, '$1').split('-'); var firstDay = $calendar[0].className.replace(/.*webform-calendar-day-(\d).*/, '$1'); // Convert date strings into actual Date objects. startDate = new Date(startDate[0], startDate[1] - 1, startDate[2]); endDate = new Date(endDate[0], endDate[1] - 1, endDate[2]); // Ensure that start comes before end for datepicker. if (startDate > endDate) { var laterDate = startDate; startDate = endDate; endDate = laterDate; } var startYear = startDate.getFullYear(); var endYear = endDate.getFullYear(); // Set up the jQuery datepicker element. $calendar.datepicker({ dateFormat: 'yy-mm-dd', yearRange: startYear + ':' + endYear, firstDay: parseInt(firstDay), minDate: startDate, maxDate: endDate, onSelect: function (dateText, inst) { var date = dateText.split('-'); $webformDatepicker.find('select.year, input.year').val(+date[0]).trigger('change'); $webformDatepicker.find('select.month').val(+date[1]).trigger('change'); $webformDatepicker.find('select.day').val(+date[2]).trigger('change'); }, beforeShow: function (input, inst) { // Get the select list values. var year = $webformDatepicker.find('select.year, input.year').val(); var month = $webformDatepicker.find('select.month').val(); var day = $webformDatepicker.find('select.day').val(); // If empty, default to the current year/month/day in the popup. var today = new Date(); year = year ? year : today.getFullYear(); month = month ? month : today.getMonth() + 1; day = day ? day : today.getDate(); // Make sure that the default year fits in the available options. year = (year < startYear || year > endYear) ? startYear : year; // jQuery UI Datepicker will read the input field and base its date // off of that, even though in our case the input field is a button. $(input).val(year + '-' + month + '-' + day); } }); // Prevent the calendar button from submitting the form. $calendar.click(function (event) { // This event is triggered also when pressing enter when the focus is on // previous webform components, but we only want to do something when // we are on the calendar component. By checking the event client x/y // position we known if it was the user clicking. For keyboard navigators // simply the focus handles the date picker so we don't have to do // anything special for them. if (event.clientX !== 0 && event.clientY !== 0) { // Focus is only necessary for Safari. But it has no impact on other // browsers. $(this).focus(); event.preventDefault(); } }); // Clear date on backspace or delete. $calendar.keyup(function (e) { if (e.keyCode == 8 || e.keyCode == 46) { $.datepicker._clearDate(this); } }); }); }; Drupal.webform.conditional = function (context) { // Add the bindings to each webform on the page. $.each(Drupal.settings.webform.conditionals, function (formKey, settings) { var $form = $('.' + formKey + ':not(.webform-conditional-processed)'); $form.each(function (index, currentForm) { var $currentForm = $(currentForm); $currentForm.addClass('webform-conditional-processed'); $currentForm.bind('change', {'settings': settings}, Drupal.webform.conditionalCheck); // Trigger all the elements that cause conditionals on this form. Drupal.webform.doConditions($currentForm, settings); }); }); }; /** * Event handler to respond to field changes in a form. * * This event is bound to the entire form, not individual fields. */ Drupal.webform.conditionalCheck = function (e) { var $triggerElement = $(e.target).closest('.webform-component'); if (!$triggerElement.length) { return; } var $form = $triggerElement.closest('form'); var triggerElementKey = $triggerElement.attr('class').match(/webform-component--[^ ]+/)[0]; var settings = e.data.settings; if (settings.sourceMap[triggerElementKey]) { Drupal.webform.doConditions($form, settings); } }; /** * Processes all conditional. */ Drupal.webform.doConditions = function ($form, settings) { var stackPointer; var resultStack; /** * Initializes an execution stack for a conditional group's rules. * * Also initializes sub-conditional rules. */ function executionStackInitialize(andor) { stackPointer = -1; resultStack = []; executionStackPush(andor); } /** * Starts a new subconditional for the given and/or operator. */ function executionStackPush(andor) { resultStack[++stackPointer] = { results: [], andor: andor, }; } /** * Adds a rule's result to the current sub-conditional. */ function executionStackAccumulate(result) { resultStack[stackPointer]['results'].push(result); } /** * Finishes a sub-conditional and adds the result to the parent stack frame. */ function executionStackPop() { // Calculate the and/or result. var stackFrame = resultStack[stackPointer]; // Pop stack and protect against stack underflow. stackPointer = Math.max(0, stackPointer - 1); var $conditionalResults = stackFrame['results']; var filteredResults = $.map($conditionalResults, function (val) { return val ? val : null; }); return stackFrame['andor'] === 'or' ? filteredResults.length > 0 : filteredResults.length === $conditionalResults.length; } // Track what has been set/hidden for each target component's elements. // Hidden elements must be disabled because if they are required and don't // have a value, they will prevent submission due to html5 validation. // Each execution of the conditionals adds a temporary class // webform-disabled-flag so that elements hidden or set can be disabled and // also be prevented from being re-enabled by another conditional (such as a // parent fieldset). After processing conditionals, this temporary class // must be removed in preparation for the next execution of the // conditionals. $.each(settings.ruleGroups, function (rgid_key, rule_group) { var ruleGroup = settings.ruleGroups[rgid_key]; // Perform the comparison callback and build the results for this group. executionStackInitialize(ruleGroup['andor']); $.each(ruleGroup['rules'], function (m, rule) { switch (rule['source_type']) { case 'component': var elementKey = rule['source']; var element = $form.find('.' + elementKey)[0]; var existingValue = settings.values[elementKey] ? settings.values[elementKey] : null; executionStackAccumulate(window['Drupal']['webform'][rule.callback](element, existingValue, rule['value'])); break; case 'conditional_start': executionStackPush(rule['andor']); break; case 'conditional_end': executionStackAccumulate(executionStackPop()); break; } }); var conditionalResult = executionStackPop(); $.each(ruleGroup['actions'], function (aid, action) { var $target = $form.find('.' + action['target']); var actionResult = action['invert'] ? !conditionalResult : conditionalResult; switch (action['action']) { case 'show': var changed = actionResult != Drupal.webform.isVisible($target); if (actionResult) { $target.find('.webform-conditional-disabled:not(.webform-disabled-flag)') .removeClass('webform-conditional-disabled') .webformProp('disabled', false); $target .removeClass('webform-conditional-hidden') .show(); $form.find('.chosen-disabled').prev().trigger('chosen:updated.chosen'); } else { $target .hide() .addClass('webform-conditional-hidden') .find(':input') .addClass('webform-conditional-disabled webform-disabled-flag') .webformProp('disabled', true); } if (changed && $target.is('tr')) { Drupal.webform.restripeTable($target.closest('table').first()); } break; case 'require': var $requiredSpan = $target.find('.form-required, .form-optional').first(); if (actionResult != $requiredSpan.hasClass('form-required')) { var $targetInputElements = $target.find("input:text,textarea,input[type='email'],select,input:radio,input:checkbox,input:file"); // Rather than hide the required tag, remove it so that other // jQuery can respond via Drupal behaviors. Drupal.detachBehaviors($requiredSpan); $targetInputElements .webformProp('required', actionResult) .toggleClass('required', actionResult); if (actionResult) { $requiredSpan.replaceWith('*'); } else { $requiredSpan.replaceWith(''); } Drupal.attachBehaviors($requiredSpan); } break; case 'set': var $texts = $target.find("input:text,textarea,input[type='email']"); var $selects = $target.find('select,select option,input:radio,input:checkbox'); var $markups = $target.filter('.webform-component-markup'); if (actionResult) { var multiple = $.map(action['argument'].split(','), $.trim); $selects .webformVal(multiple) .webformProp('disabled', true) .addClass('webform-disabled-flag'); $texts .val([action['argument']]) .webformProp('readonly', true) .addClass('webform-disabled-flag'); // A special case is made for markup. It is sanitized with // filter_xss_admin on the server. otherwise text() should be used // to avoid an XSS vulnerability. text() however would preclude // the use of tags like or . $markups.html(action['argument']); } else { $selects.not('.webform-disabled-flag') .webformProp('disabled', false); $texts.not('.webform-disabled-flag') .webformProp('readonly', false); // Markup not set? Then restore original markup as provided in // the attribute data-webform-markup. $markups.each(function () { var $this = $(this); var original = $this.data('webform-markup'); if (original !== undefined) { $this.html(original); } }); } break; } }); // End look on each action for one conditional. }); // End loop on each conditional. $form.find('.webform-disabled-flag').removeClass('webform-disabled-flag'); }; /** * Event handler to prevent propagation of events. * * Typically click for disabling radio and checkboxes. */ Drupal.webform.stopEvent = function () { return false; }; Drupal.webform.conditionalOperatorStringEqual = function (element, existingValue, ruleValue) { var returnValue = false; var currentValue = Drupal.webform.stringValue(element, existingValue); $.each(currentValue, function (n, value) { if (value.toLowerCase() === ruleValue.toLowerCase()) { returnValue = true; return false; // break. } }); return returnValue; }; Drupal.webform.conditionalOperatorStringNotEqual = function (element, existingValue, ruleValue) { var found = false; var currentValue = Drupal.webform.stringValue(element, existingValue); $.each(currentValue, function (n, value) { if (value.toLowerCase() === ruleValue.toLowerCase()) { found = true; } }); return !found; }; Drupal.webform.conditionalOperatorStringContains = function (element, existingValue, ruleValue) { var returnValue = false; var currentValue = Drupal.webform.stringValue(element, existingValue); $.each(currentValue, function (n, value) { if (value.toLowerCase().indexOf(ruleValue.toLowerCase()) > -1) { returnValue = true; return false; // break. } }); return returnValue; }; Drupal.webform.conditionalOperatorStringDoesNotContain = function (element, existingValue, ruleValue) { var found = false; var currentValue = Drupal.webform.stringValue(element, existingValue); $.each(currentValue, function (n, value) { if (value.toLowerCase().indexOf(ruleValue.toLowerCase()) > -1) { found = true; } }); return !found; }; Drupal.webform.conditionalOperatorStringBeginsWith = function (element, existingValue, ruleValue) { var returnValue = false; var currentValue = Drupal.webform.stringValue(element, existingValue); $.each(currentValue, function (n, value) { if (value.toLowerCase().indexOf(ruleValue.toLowerCase()) === 0) { returnValue = true; return false; // break. } }); return returnValue; }; Drupal.webform.conditionalOperatorStringEndsWith = function (element, existingValue, ruleValue) { var returnValue = false; var currentValue = Drupal.webform.stringValue(element, existingValue); $.each(currentValue, function (n, value) { if (value.toLowerCase().lastIndexOf(ruleValue.toLowerCase()) === value.length - ruleValue.length) { returnValue = true; return false; // break. } }); return returnValue; }; Drupal.webform.conditionalOperatorStringEmpty = function (element, existingValue, ruleValue) { var currentValue = Drupal.webform.stringValue(element, existingValue); var returnValue = true; $.each(currentValue, function (n, value) { if (value !== '') { returnValue = false; return false; // break. } }); return returnValue; }; Drupal.webform.conditionalOperatorStringNotEmpty = function (element, existingValue, ruleValue) { return !Drupal.webform.conditionalOperatorStringEmpty(element, existingValue, ruleValue); }; Drupal.webform.conditionalOperatorSelectGreaterThan = function (element, existingValue, ruleValue) { var currentValue = Drupal.webform.stringValue(element, existingValue); return Drupal.webform.compare_select(currentValue[0], ruleValue, element) > 0; }; Drupal.webform.conditionalOperatorSelectGreaterThanEqual = function (element, existingValue, ruleValue) { var currentValue = Drupal.webform.stringValue(element, existingValue); var comparison = Drupal.webform.compare_select(currentValue[0], ruleValue, element); return comparison > 0 || comparison === 0; }; Drupal.webform.conditionalOperatorSelectLessThan = function (element, existingValue, ruleValue) { var currentValue = Drupal.webform.stringValue(element, existingValue); return Drupal.webform.compare_select(currentValue[0], ruleValue, element) < 0; }; Drupal.webform.conditionalOperatorSelectLessThanEqual = function (element, existingValue, ruleValue) { var currentValue = Drupal.webform.stringValue(element, existingValue); var comparison = Drupal.webform.compare_select(currentValue[0], ruleValue, element); return comparison < 0 || comparison === 0; }; Drupal.webform.conditionalOperatorNumericEqual = function (element, existingValue, ruleValue) { // See float comparison: http://php.net/manual/en/language.types.float.php var currentValue = Drupal.webform.stringValue(element, existingValue); var epsilon = 0.000001; // An empty string does not match any number. return currentValue[0] === '' ? false : (Math.abs(parseFloat(currentValue[0]) - parseFloat(ruleValue)) < epsilon); }; Drupal.webform.conditionalOperatorNumericNotEqual = function (element, existingValue, ruleValue) { // See float comparison: http://php.net/manual/en/language.types.float.php var currentValue = Drupal.webform.stringValue(element, existingValue); var epsilon = 0.000001; // An empty string does not match any number. return currentValue[0] === '' ? true : (Math.abs(parseFloat(currentValue[0]) - parseFloat(ruleValue)) >= epsilon); }; Drupal.webform.conditionalOperatorNumericGreaterThan = function (element, existingValue, ruleValue) { var currentValue = Drupal.webform.stringValue(element, existingValue); return parseFloat(currentValue[0]) > parseFloat(ruleValue); }; Drupal.webform.conditionalOperatorNumericGreaterThanEqual = function (element, existingValue, ruleValue) { return Drupal.webform.conditionalOperatorNumericGreaterThan(element, existingValue, ruleValue) || Drupal.webform.conditionalOperatorNumericEqual(element, existingValue, ruleValue); }; Drupal.webform.conditionalOperatorNumericLessThan = function (element, existingValue, ruleValue) { var currentValue = Drupal.webform.stringValue(element, existingValue); return parseFloat(currentValue[0]) < parseFloat(ruleValue); }; Drupal.webform.conditionalOperatorNumericLessThanEqual = function (element, existingValue, ruleValue) { return Drupal.webform.conditionalOperatorNumericLessThan(element, existingValue, ruleValue) || Drupal.webform.conditionalOperatorNumericEqual(element, existingValue, ruleValue); }; Drupal.webform.conditionalOperatorDateEqual = function (element, existingValue, ruleValue) { var currentValue = Drupal.webform.dateValue(element, existingValue); return currentValue === ruleValue; }; Drupal.webform.conditionalOperatorDateNotEqual = function (element, existingValue, ruleValue) { return !Drupal.webform.conditionalOperatorDateEqual(element, existingValue, ruleValue); }; Drupal.webform.conditionalOperatorDateBefore = function (element, existingValue, ruleValue) { var currentValue = Drupal.webform.dateValue(element, existingValue); return (currentValue !== false) && currentValue < ruleValue; }; Drupal.webform.conditionalOperatorDateBeforeEqual = function (element, existingValue, ruleValue) { var currentValue = Drupal.webform.dateValue(element, existingValue); return (currentValue !== false) && (currentValue < ruleValue || currentValue === ruleValue); }; Drupal.webform.conditionalOperatorDateAfter = function (element, existingValue, ruleValue) { var currentValue = Drupal.webform.dateValue(element, existingValue); return (currentValue !== false) && currentValue > ruleValue; }; Drupal.webform.conditionalOperatorDateAfterEqual = function (element, existingValue, ruleValue) { var currentValue = Drupal.webform.dateValue(element, existingValue); return (currentValue !== false) && (currentValue > ruleValue || currentValue === ruleValue); }; Drupal.webform.conditionalOperatorTimeEqual = function (element, existingValue, ruleValue) { var currentValue = Drupal.webform.timeValue(element, existingValue); return currentValue === ruleValue; }; Drupal.webform.conditionalOperatorTimeNotEqual = function (element, existingValue, ruleValue) { return !Drupal.webform.conditionalOperatorTimeEqual(element, existingValue, ruleValue); }; Drupal.webform.conditionalOperatorTimeBefore = function (element, existingValue, ruleValue) { // Date and time operators intentionally exclusive for "before". var currentValue = Drupal.webform.timeValue(element, existingValue); return (currentValue !== false) && (currentValue < ruleValue); }; Drupal.webform.conditionalOperatorTimeBeforeEqual = function (element, existingValue, ruleValue) { // Date and time operators intentionally exclusive for "before". var currentValue = Drupal.webform.timeValue(element, existingValue); return (currentValue !== false) && (currentValue < ruleValue || currentValue === ruleValue); }; Drupal.webform.conditionalOperatorTimeAfter = function (element, existingValue, ruleValue) { // Date and time operators intentionally inclusive for "after". var currentValue = Drupal.webform.timeValue(element, existingValue); return (currentValue !== false) && (currentValue > ruleValue); }; Drupal.webform.conditionalOperatorTimeAfterEqual = function (element, existingValue, ruleValue) { // Date and time operators intentionally inclusive for "after". var currentValue = Drupal.webform.timeValue(element, existingValue); return (currentValue !== false) && (currentValue > ruleValue || currentValue === ruleValue); }; /** * Utility function to compare values of a select component. * * @param string a * First select option key to compare * @param string b * Second select option key to compare * @param array options * Associative array where the a and b are within the keys * * @return integer based upon position of $a and $b in $options * -N if $a above (<) $b * 0 if $a = $b * +N if $a is below (>) $b */ Drupal.webform.compare_select = function (a, b, element) { var optionList = []; $('option,input:radio,input:checkbox', element).each(function () { optionList.push($(this).val()); }); var a_position = optionList.indexOf(a); var b_position = optionList.indexOf(b); return (a_position < 0 || b_position < 0) ? null : a_position - b_position; }; /** * Utility to return current visibility. * * Uses actual visibility, except for hidden components which use the applied * disabled class. */ Drupal.webform.isVisible = function ($element) { return $element.hasClass('webform-component-hidden') ? !$element.find('input').first().hasClass('webform-conditional-disabled') : $element.closest('.webform-conditional-hidden').length == 0; }; /** * Function to get a string value from a select/radios/text/etc. field. */ Drupal.webform.stringValue = function (element, existingValue) { var value = []; if (element) { var $element = $(element); if (Drupal.webform.isVisible($element)) { // Checkboxes and radios. $element.find('input[type=checkbox]:checked,input[type=radio]:checked').each(function () { value.push(this.value); }); // Select lists. if (!value.length) { var selectValue = $element.find('select').val(); if (selectValue) { if ($.isArray(selectValue)) { value = selectValue; } else { value.push(selectValue); } } } // Simple text fields. This check is done last so that the select list // in select-or-other fields comes before the "other" text field. if (!value.length) { $element.find('input:not([type=checkbox],[type=radio]),textarea').each(function () { value.push(this.value); }); } } } else { switch ($.type(existingValue)) { case 'array': value = existingValue; break; case 'string': value.push(existingValue); break; } } return value; }; /** * Utility function to calculate a second-based timestamp from a time field. */ Drupal.webform.dateValue = function (element, existingValue) { var value = false; if (element) { var $element = $(element); if (Drupal.webform.isVisible($element)) { var day = $element.find('[name*=day]').val(); var month = $element.find('[name*=month]').val(); var year = $element.find('[name*=year]').val(); // Months are 0 indexed in JavaScript. if (month) { month--; } if (year !== '' && month !== '' && day !== '') { value = Date.UTC(year, month, day) / 1000; } } } else { if ($.type(existingValue) === 'array' && existingValue.length) { existingValue = existingValue[0]; } if ($.type(existingValue) === 'string') { existingValue = existingValue.split('-'); } if (existingValue.length === 3) { value = Date.UTC(existingValue[0], existingValue[1], existingValue[2]) / 1000; } } return value; }; /** * Utility function to calculate a millisecond timestamp from a time field. */ Drupal.webform.timeValue = function (element, existingValue) { var value = false; if (element) { var $element = $(element); if (Drupal.webform.isVisible($element)) { var hour = $element.find('[name*=hour]').val(); var minute = $element.find('[name*=minute]').val(); var ampm = $element.find('[name*=ampm]:checked').val(); // Convert to integers if set. hour = (hour === '') ? hour : parseInt(hour); minute = (minute === '') ? minute : parseInt(minute); if (hour !== '') { hour = (hour < 12 && ampm == 'pm') ? hour + 12 : hour; hour = (hour === 12 && ampm == 'am') ? 0 : hour; } if (hour !== '' && minute !== '') { value = Date.UTC(1970, 0, 1, hour, minute) / 1000; } } } else { if ($.type(existingValue) === 'array' && existingValue.length) { existingValue = existingValue[0]; } if ($.type(existingValue) === 'string') { existingValue = existingValue.split(':'); } if (existingValue.length >= 2) { value = Date.UTC(1970, 0, 1, existingValue[0], existingValue[1]) / 1000; } } return value; }; /** * Make a prop shim for jQuery < 1.9. */ $.fn.webformProp = $.fn.webformProp || function (name, value) { if (value) { return $.fn.prop ? this.prop(name, true) : this.attr(name, true); } else { return $.fn.prop ? this.prop(name, false) : this.removeAttr(name); } }; /** * Make a multi-valued val() function. * * This is for setting checkboxes, radios, and select elements. */ $.fn.webformVal = function (values) { this.each(function () { var $this = $(this); var value = $this.val(); var on = $.inArray($this.val(), values) != -1; if (this.nodeName == 'OPTION') { $this.webformProp('selected', on ? value : false); } else { $this.val(on ? [value] : false); } }); return this; }; /** * Given a table's DOM element, restripe the odd/even classes. */ Drupal.webform.restripeTable = function (table) { // :even and :odd are reversed because jQuery counts from 0 and // we count from 1, so we're out of sync. // Match immediate children of the parent element to allow nesting. $('> tbody > tr, > tr', table) .filter(':visible:odd').filter('.odd') .removeClass('odd').addClass('even') .end().end() .filter(':visible:even').filter('.even') .removeClass('even').addClass('odd'); }; })(jQuery);