3 * JavaScript behaviors for the front-end display of webforms.
10 Drupal
.behaviors
.webform
= Drupal
.behaviors
.webform
|| {};
12 Drupal
.behaviors
.webform
.attach = function (context
) {
13 // Calendar datepicker behavior.
14 Drupal
.webform
.datepicker(context
);
17 if (Drupal
.settings
.webform
&& Drupal
.settings
.webform
.conditionals
) {
18 Drupal
.webform
.conditional(context
);
22 Drupal
.webform
= Drupal
.webform
|| {};
24 Drupal
.webform
.datepicker = function (context
) {
25 $('div.webform-datepicker').each(function () {
26 var $webformDatepicker
= $(this);
27 var $calendar
= $webformDatepicker
.find('input.webform-calendar');
29 // Ensure the page we're on actually contains a datepicker.
30 if ($calendar
.length
== 0) {
34 var startDate
= $calendar
[0].className
.replace(/.*webform-calendar-start-(\d{4}-\d{2}-\d{2}).*/, '$1').split('-');
35 var endDate
= $calendar
[0].className
.replace(/.*webform-calendar-end-(\d{4}-\d{2}-\d{2}).*/, '$1').split('-');
36 var firstDay
= $calendar
[0].className
.replace(/.*webform-calendar-day-(\d).*/, '$1');
37 // Convert date strings into actual Date objects.
38 startDate
= new Date(startDate
[0], startDate
[1] - 1, startDate
[2]);
39 endDate
= new Date(endDate
[0], endDate
[1] - 1, endDate
[2]);
41 // Ensure that start comes before end for datepicker.
42 if (startDate
> endDate
) {
43 var laterDate
= startDate
;
48 var startYear
= startDate
.getFullYear();
49 var endYear
= endDate
.getFullYear();
51 // Set up the jQuery datepicker element.
52 $calendar
.datepicker({
53 dateFormat
: 'yy-mm-dd',
54 yearRange
: startYear
+ ':' + endYear
,
55 firstDay
: parseInt(firstDay
),
58 onSelect: function (dateText
, inst
) {
59 var date
= dateText
.split('-');
60 $webformDatepicker
.find('select.year, input.year').val(+date
[0]).trigger('change');
61 $webformDatepicker
.find('select.month').val(+date
[1]).trigger('change');
62 $webformDatepicker
.find('select.day').val(+date
[2]).trigger('change');
64 beforeShow: function (input
, inst
) {
65 // Get the select list values.
66 var year
= $webformDatepicker
.find('select.year, input.year').val();
67 var month
= $webformDatepicker
.find('select.month').val();
68 var day
= $webformDatepicker
.find('select.day').val();
70 // If empty, default to the current year/month/day in the popup.
71 var today
= new Date();
72 year
= year
? year
: today
.getFullYear();
73 month
= month
? month
: today
.getMonth() + 1;
74 day
= day
? day
: today
.getDate();
76 // Make sure that the default year fits in the available options.
77 year
= (year
< startYear
|| year
> endYear
) ? startYear
: year
;
79 // jQuery UI Datepicker will read the input field and base its date off
80 // of that, even though in our case the input field is a button.
81 $(input
).val(year
+ '-' + month
+ '-' + day
);
85 // Prevent the calendar button from submitting the form.
86 $calendar
.click(function (event
) {
88 event
.preventDefault();
93 Drupal
.webform
.conditional = function (context
) {
94 // Add the bindings to each webform on the page.
95 $.each(Drupal
.settings
.webform
.conditionals
, function (formKey
, settings
) {
96 var $form
= $('.' + formKey
+ ':not(.webform-conditional-processed)');
97 $form
.each(function (index
, currentForm
) {
98 var $currentForm
= $(currentForm
);
99 $currentForm
.addClass('webform-conditional-processed');
100 $currentForm
.bind('change', {'settings': settings
}, Drupal
.webform
.conditionalCheck
);
102 // Trigger all the elements that cause conditionals on this form.
103 Drupal
.webform
.doConditions($form
, settings
);
109 * Event handler to respond to field changes in a form.
111 * This event is bound to the entire form, not individual fields.
113 Drupal
.webform
.conditionalCheck = function (e
) {
114 var $triggerElement
= $(e
.target
).closest('.webform-component');
115 var $form
= $triggerElement
.closest('form');
116 var triggerElementKey
= $triggerElement
.attr('class').match(/webform-component--[^ ]+/)[0];
117 var settings
= e
.data
.settings
;
118 if (settings
.sourceMap
[triggerElementKey
]) {
119 Drupal
.webform
.doConditions($form
, settings
);
124 * Processes all conditional.
126 Drupal
.webform
.doConditions = function ($form
, settings
) {
132 * Initializes an execution stack for a conditional group's rules and
133 * sub-conditional rules.
135 function executionStackInitialize(andor
) {
138 executionStackPush(andor
);
142 * Starts a new subconditional for the given and/or operator.
144 function executionStackPush(andor
) {
145 resultStack
[++stackPointer
] = {
152 * Adds a rule's result to the current sub-condtional.
154 function executionStackAccumulate(result
) {
155 resultStack
[stackPointer
]['results'].push(result
);
159 * Finishes a sub-conditional and adds the result to the parent stack frame.
161 function executionStackPop() {
162 // Calculate the and/or result.
163 var stackFrame
= resultStack
[stackPointer
];
164 // Pop stack and protect against stack underflow.
165 stackPointer
= Math
.max(0, stackPointer
- 1);
166 var $conditionalResults
= stackFrame
['results'];
167 var filteredResults
= $.map($conditionalResults
, function(val
) {
168 return val
? val
: null;
170 return stackFrame
['andor'] === 'or'
171 ? filteredResults
.length
> 0
172 : filteredResults
.length
=== $conditionalResults
.length
;
175 // Track what has be set/shown for each target component.
176 var targetLocked
= [];
178 $.each(settings
.ruleGroups
, function (rgid_key
, rule_group
) {
179 var ruleGroup
= settings
.ruleGroups
[rgid_key
];
181 // Perform the comparison callback and build the results for this group.
182 executionStackInitialize(ruleGroup
['andor']);
183 $.each(ruleGroup
['rules'], function (m
, rule
) {
184 switch (rule
['source_type']) {
186 var elementKey
= rule
['source'];
187 var element
= $form
.find('.' + elementKey
)[0];
188 var existingValue
= settings
.values
[elementKey
] ? settings
.values
[elementKey
] : null;
189 executionStackAccumulate(window
['Drupal']['webform'][rule
.callback
](element
, existingValue
, rule
['value']));
191 case 'conditional_start':
192 executionStackPush(rule
['andor']);
194 case 'conditional_end':
195 executionStackAccumulate(executionStackPop());
199 var conditionalResult
= executionStackPop();
201 $.each(ruleGroup
['actions'], function (aid
, action
) {
202 var $target
= $form
.find('.' + action
['target']);
203 var actionResult
= action
['invert'] ? !conditionalResult
: conditionalResult
;
204 switch (action
['action']) {
206 if (actionResult
!= Drupal
.webform
.isVisible($target
)) {
207 var $targetElements
= actionResult
208 ? $target
.find('.webform-conditional-disabled').removeClass('webform-conditional-disabled')
209 : $target
.find(':input').addClass('webform-conditional-disabled');
210 $targetElements
.webformProp('disabled', !actionResult
);
211 $target
.toggleClass('webform-conditional-hidden', !actionResult
);
217 // Record that the target was hidden.
218 targetLocked
[action
['target']] = 'hide';
220 if ($target
.is('tr')) {
221 Drupal
.webform
.restripeTable($target
.closest('table').first());
226 var $requiredSpan
= $target
.find('.form-required, .form-optional').first();
227 if (actionResult
!= $requiredSpan
.hasClass('form-required')) {
228 var $targetInputElements
= $target
.find("input:text,textarea,input[type='email'],select,input:radio,input:file");
229 // Rather than hide the required tag, remove it so that other jQuery can respond via Drupal behaviors.
230 Drupal
.detachBehaviors($requiredSpan
);
232 .webformProp('required', actionResult
)
233 .toggleClass('required', actionResult
);
235 $requiredSpan
.replaceWith('<span class="form-required" title="' + Drupal
.t('This field is required.') + '">*</span>');
238 $requiredSpan
.replaceWith('<span class="form-optional"></span>');
240 Drupal
.attachBehaviors($requiredSpan
);
244 var isLocked
= targetLocked
[action
['target']];
245 var $texts
= $target
.find("input:text,textarea,input[type='email']");
246 var $selects
= $target
.find('select,select option,input:radio,input:checkbox');
247 var $markups
= $target
.filter('.webform-component-markup');
249 var multiple
= $.map(action
['argument'].split(','), $.trim
);
250 $selects
.webformVal(multiple
);
251 $texts
.val([action
['argument']]);
252 // A special case is made for markup. It is sanitized with filter_xss_admin on the server.
253 // otherwise text() should be used to avoid an XSS vulnerability. text() however would
254 // preclude the use of tags like <strong> or <a>
255 $markups
.html(action
['argument']);
258 // Markup not set? Then restore original markup as provided in
259 // the attribute data-webform-markup.
260 $markups
.each(function() {
262 var original
= $this.data('webform-markup');
263 if (original
!== undefined) {
264 $this.html(original
);
269 // If not previously hidden or set, disable the element readonly or readonly-like behavior.
270 $selects
.webformProp('disabled', actionResult
);
271 $texts
.webformProp('readonly', actionResult
);
272 targetLocked
[action
['target']] = actionResult
? 'set' : false;
276 }); // End look on each action for one conditional
277 }); // End loop on each conditional
281 * Event handler to prevent propogation of events, typically click for disabling
282 * radio and checkboxes.
284 Drupal
.webform
.stopEvent = function () {
288 Drupal
.webform
.conditionalOperatorStringEqual = function (element
, existingValue
, ruleValue
) {
289 var returnValue
= false;
290 var currentValue
= Drupal
.webform
.stringValue(element
, existingValue
);
291 $.each(currentValue
, function (n
, value
) {
292 if (value
.toLowerCase() === ruleValue
.toLowerCase()) {
294 return false; // break.
300 Drupal
.webform
.conditionalOperatorStringNotEqual = function (element
, existingValue
, ruleValue
) {
302 var currentValue
= Drupal
.webform
.stringValue(element
, existingValue
);
303 $.each(currentValue
, function (n
, value
) {
304 if (value
.toLowerCase() === ruleValue
.toLowerCase()) {
311 Drupal
.webform
.conditionalOperatorStringContains = function (element
, existingValue
, ruleValue
) {
312 var returnValue
= false;
313 var currentValue
= Drupal
.webform
.stringValue(element
, existingValue
);
314 $.each(currentValue
, function (n
, value
) {
315 if (value
.toLowerCase().indexOf(ruleValue
.toLowerCase()) > -1) {
317 return false; // break.
323 Drupal
.webform
.conditionalOperatorStringDoesNotContain = function (element
, existingValue
, ruleValue
) {
325 var currentValue
= Drupal
.webform
.stringValue(element
, existingValue
);
326 $.each(currentValue
, function (n
, value
) {
327 if (value
.toLowerCase().indexOf(ruleValue
.toLowerCase()) > -1) {
334 Drupal
.webform
.conditionalOperatorStringBeginsWith = function (element
, existingValue
, ruleValue
) {
335 var returnValue
= false;
336 var currentValue
= Drupal
.webform
.stringValue(element
, existingValue
);
337 $.each(currentValue
, function (n
, value
) {
338 if (value
.toLowerCase().indexOf(ruleValue
.toLowerCase()) === 0) {
340 return false; // break.
346 Drupal
.webform
.conditionalOperatorStringEndsWith = function (element
, existingValue
, ruleValue
) {
347 var returnValue
= false;
348 var currentValue
= Drupal
.webform
.stringValue(element
, existingValue
);
349 $.each(currentValue
, function (n
, value
) {
350 if (value
.toLowerCase().lastIndexOf(ruleValue
.toLowerCase()) === value
.length
- ruleValue
.length
) {
352 return false; // break.
358 Drupal
.webform
.conditionalOperatorStringEmpty = function (element
, existingValue
, ruleValue
) {
359 var currentValue
= Drupal
.webform
.stringValue(element
, existingValue
);
360 var returnValue
= true;
361 $.each(currentValue
, function (n
, value
) {
364 return false; // break.
370 Drupal
.webform
.conditionalOperatorStringNotEmpty = function (element
, existingValue
, ruleValue
) {
371 return !Drupal
.webform
.conditionalOperatorStringEmpty(element
, existingValue
, ruleValue
);
374 Drupal
.webform
.conditionalOperatorSelectGreaterThan = function (element
, existingValue
, ruleValue
) {
375 var currentValue
= Drupal
.webform
.stringValue(element
, existingValue
);
376 return Drupal
.webform
.compare_select(currentValue
[0], ruleValue
, element
) > 0;
379 Drupal
.webform
.conditionalOperatorSelectGreaterThanEqual = function (element
, existingValue
, ruleValue
) {
380 var currentValue
= Drupal
.webform
.stringValue(element
, existingValue
);
381 var comparison
= Drupal
.webform
.compare_select(currentValue
[0], ruleValue
, element
);
382 return comparison
> 0 || comparison
=== 0;
385 Drupal
.webform
.conditionalOperatorSelectLessThan = function (element
, existingValue
, ruleValue
) {
386 var currentValue
= Drupal
.webform
.stringValue(element
, existingValue
);
387 return Drupal
.webform
.compare_select(currentValue
[0], ruleValue
, element
) < 0;
390 Drupal
.webform
.conditionalOperatorSelectLessThanEqual = function (element
, existingValue
, ruleValue
) {
391 var currentValue
= Drupal
.webform
.stringValue(element
, existingValue
);
392 var comparison
= Drupal
.webform
.compare_select(currentValue
[0], ruleValue
, element
);
393 return comparison
< 0 || comparison
=== 0;
396 Drupal
.webform
.conditionalOperatorNumericEqual = function (element
, existingValue
, ruleValue
) {
397 // See float comparison: http://php.net/manual/en/language.types.float.php
398 var currentValue
= Drupal
.webform
.stringValue(element
, existingValue
);
399 var epsilon
= 0.000001;
400 // An empty string does not match any number.
401 return currentValue
[0] === '' ? false : (Math
.abs(parseFloat(currentValue
[0]) - parseFloat(ruleValue
)) < epsilon
);
404 Drupal
.webform
.conditionalOperatorNumericNotEqual = function (element
, existingValue
, ruleValue
) {
405 // See float comparison: http://php.net/manual/en/language.types.float.php
406 var currentValue
= Drupal
.webform
.stringValue(element
, existingValue
);
407 var epsilon
= 0.000001;
408 // An empty string does not match any number.
409 return currentValue
[0] === '' ? true : (Math
.abs(parseFloat(currentValue
[0]) - parseFloat(ruleValue
)) >= epsilon
);
412 Drupal
.webform
.conditionalOperatorNumericGreaterThan = function (element
, existingValue
, ruleValue
) {
413 var currentValue
= Drupal
.webform
.stringValue(element
, existingValue
);
414 return parseFloat(currentValue
[0]) > parseFloat(ruleValue
);
417 Drupal
.webform
.conditionalOperatorNumericGreaterThanEqual = function (element
, existingValue
, ruleValue
) {
418 return Drupal
.webform
.conditionalOperatorNumericGreaterThan(element
, existingValue
, ruleValue
) ||
419 Drupal
.webform
.conditionalOperatorNumericEqual(element
, existingValue
, ruleValue
);
422 Drupal
.webform
.conditionalOperatorNumericLessThan = function (element
, existingValue
, ruleValue
) {
423 var currentValue
= Drupal
.webform
.stringValue(element
, existingValue
);
424 return parseFloat(currentValue
[0]) < parseFloat(ruleValue
);
427 Drupal
.webform
.conditionalOperatorNumericLessThanEqual = function (element
, existingValue
, ruleValue
) {
428 return Drupal
.webform
.conditionalOperatorNumericLessThan(element
, existingValue
, ruleValue
) ||
429 Drupal
.webform
.conditionalOperatorNumericEqual(element
, existingValue
, ruleValue
);
432 Drupal
.webform
.conditionalOperatorDateEqual = function (element
, existingValue
, ruleValue
) {
433 var currentValue
= Drupal
.webform
.dateValue(element
, existingValue
);
434 return currentValue
=== ruleValue
;
437 Drupal
.webform
.conditionalOperatorDateNotEqual = function (element
, existingValue
, ruleValue
) {
438 return !Drupal
.webform
.conditionalOperatorDateEqual(element
, existingValue
, ruleValue
);
441 Drupal
.webform
.conditionalOperatorDateBefore = function (element
, existingValue
, ruleValue
) {
442 var currentValue
= Drupal
.webform
.dateValue(element
, existingValue
);
443 return (currentValue
!== false) && currentValue
< ruleValue
;
446 Drupal
.webform
.conditionalOperatorDateBeforeEqual = function (element
, existingValue
, ruleValue
) {
447 var currentValue
= Drupal
.webform
.dateValue(element
, existingValue
);
448 return (currentValue
!== false) && (currentValue
< ruleValue
|| currentValue
=== ruleValue
);
451 Drupal
.webform
.conditionalOperatorDateAfter = function (element
, existingValue
, ruleValue
) {
452 var currentValue
= Drupal
.webform
.dateValue(element
, existingValue
);
453 return (currentValue
!== false) && currentValue
> ruleValue
;
456 Drupal
.webform
.conditionalOperatorDateAfterEqual = function (element
, existingValue
, ruleValue
) {
457 var currentValue
= Drupal
.webform
.dateValue(element
, existingValue
);
458 return (currentValue
!== false) && (currentValue
> ruleValue
|| currentValue
=== ruleValue
);
461 Drupal
.webform
.conditionalOperatorTimeEqual = function (element
, existingValue
, ruleValue
) {
462 var currentValue
= Drupal
.webform
.timeValue(element
, existingValue
);
463 return currentValue
=== ruleValue
;
466 Drupal
.webform
.conditionalOperatorTimeNotEqual = function (element
, existingValue
, ruleValue
) {
467 return !Drupal
.webform
.conditionalOperatorTimeEqual(element
, existingValue
, ruleValue
);
470 Drupal
.webform
.conditionalOperatorTimeBefore = function (element
, existingValue
, ruleValue
) {
471 // Date and time operators intentionally exclusive for "before".
472 var currentValue
= Drupal
.webform
.timeValue(element
, existingValue
);
473 return (currentValue
!== false) && (currentValue
< ruleValue
);
476 Drupal
.webform
.conditionalOperatorTimeBeforeEqual = function (element
, existingValue
, ruleValue
) {
477 // Date and time operators intentionally exclusive for "before".
478 var currentValue
= Drupal
.webform
.timeValue(element
, existingValue
);
479 return (currentValue
!== false) && (currentValue
< ruleValue
|| currentValue
=== ruleValue
);
482 Drupal
.webform
.conditionalOperatorTimeAfter = function (element
, existingValue
, ruleValue
) {
483 // Date and time operators intentionally inclusive for "after".
484 var currentValue
= Drupal
.webform
.timeValue(element
, existingValue
);
485 return (currentValue
!== false) && (currentValue
> ruleValue
);
488 Drupal
.webform
.conditionalOperatorTimeAfterEqual = function (element
, existingValue
, ruleValue
) {
489 // Date and time operators intentionally inclusive for "after".
490 var currentValue
= Drupal
.webform
.timeValue(element
, existingValue
);
491 return (currentValue
!== false) && (currentValue
> ruleValue
|| currentValue
=== ruleValue
);
495 * Utility function to compare values of a select component.
497 * First select option key to compare
499 * Second select option key to compare
500 * @param array options
501 * Associative array where the a and b are within the keys
502 * @return integer based upon position of $a and $b in $options
503 * -N if $a above (<) $b
505 * +N if $a is below (>) $b
507 Drupal
.webform
.compare_select = function (a
, b
, element
) {
509 $('option,input:radio,input:checkbox', element
).each(function () {
510 optionList
.push($(this).val());
512 var a_position
= optionList
.indexOf(a
);
513 var b_position
= optionList
.indexOf(b
);
514 return (a_position
< 0 || b_position
< 0) ? null : a_position
- b_position
;
518 * Utility to return current visibility. Uses actual visibility, except for
519 * hidden components which use the applied disabled class.
521 Drupal
.webform
.isVisible = function ($element
) {
522 return $element
.hasClass('webform-component-hidden')
523 ? !$element
.find('input').first().hasClass('webform-conditional-disabled')
524 : $element
.closest('.webform-conditional-hidden').length
== 0;
528 * Utility function to get a string value from a select/radios/text/etc. field.
530 Drupal
.webform
.stringValue = function (element
, existingValue
) {
533 var $element
= $(element
);
534 if (Drupal
.webform
.isVisible($element
)) {
535 // Checkboxes and radios.
536 $element
.find('input[type=checkbox]:checked,input[type=radio]:checked').each(function () {
537 value
.push(this.value
);
541 var selectValue
= $element
.find('select').val();
543 if ($.isArray(selectValue
)) {
547 value
.push(selectValue
);
551 // Simple text fields. This check is done last so that the select list in
552 // select-or-other fields comes before the "other" text field.
554 $element
.find('input:not([type=checkbox],[type=radio]),textarea').each(function () {
555 value
.push(this.value
);
561 switch ($.type(existingValue
)) {
563 value
= existingValue
;
566 value
.push(existingValue
);
574 * Utility function to calculate a second-based timestamp from a time field.
576 Drupal
.webform
.dateValue = function (element
, existingValue
) {
579 var $element
= $(element
);
580 if (Drupal
.webform
.isVisible($element
)) {
581 var day
= $element
.find('[name*=day]').val();
582 var month
= $element
.find('[name*=month]').val();
583 var year
= $element
.find('[name*=year]').val();
584 // Months are 0 indexed in JavaScript.
588 if (year
!== '' && month
!== '' && day
!== '') {
589 value
= Date
.UTC(year
, month
, day
) / 1000;
594 if ($.type(existingValue
) === 'array' && existingValue
.length
) {
595 existingValue
= existingValue
[0];
597 if ($.type(existingValue
) === 'string') {
598 existingValue
= existingValue
.split('-');
600 if (existingValue
.length
=== 3) {
601 value
= Date
.UTC(existingValue
[0], existingValue
[1], existingValue
[2]) / 1000;
608 * Utility function to calculate a millisecond timestamp from a time field.
610 Drupal
.webform
.timeValue = function (element
, existingValue
) {
613 var $element
= $(element
);
614 if (Drupal
.webform
.isVisible($element
)) {
615 var hour
= $element
.find('[name*=hour]').val();
616 var minute
= $element
.find('[name*=minute]').val();
617 var ampm
= $element
.find('[name*=ampm]:checked').val();
619 // Convert to integers if set.
620 hour
= (hour
=== '') ? hour
: parseInt(hour
);
621 minute
= (minute
=== '') ? minute
: parseInt(minute
);
624 hour
= (hour
< 12 && ampm
== 'pm') ? hour
+ 12 : hour
;
625 hour
= (hour
=== 12 && ampm
== 'am') ? 0 : hour
;
627 if (hour
!== '' && minute
!== '') {
628 value
= Date
.UTC(1970, 0, 1, hour
, minute
) / 1000;
633 if ($.type(existingValue
) === 'array' && existingValue
.length
) {
634 existingValue
= existingValue
[0];
636 if ($.type(existingValue
) === 'string') {
637 existingValue
= existingValue
.split(':');
639 if (existingValue
.length
>= 2) {
640 value
= Date
.UTC(1970, 0, 1, existingValue
[0], existingValue
[1]) / 1000;
647 * Make a prop shim for jQuery < 1.9.
649 $.fn
.webformProp
= $.fn
.webformProp
|| function (name
, value
) {
651 return $.fn
.prop
? this.prop(name
, true) : this.attr(name
, true);
654 return $.fn
.prop
? this.prop(name
, false) : this.removeAttr(name
);
659 * Make a multi-valued val() function for setting checkboxes, radios, and select
662 $.fn
.webformVal = function (values
) {
663 this.each(function () {
665 var value
= $this.val();
666 var on
= $.inArray($this.val(), values
) != -1;
667 if (this.nodeName
== 'OPTION') {
668 $this.webformProp('selected', on
? value
: false);
671 $this.val(on
? [value
] : false);
678 * Given a table's DOM element, restripe the odd/even classes.
680 Drupal
.webform
.restripeTable = function (table
) {
681 // :even and :odd are reversed because jQuery counts from 0 and
682 // we count from 1, so we're out of sync.
683 // Match immediate children of the parent element to allow nesting.
684 $('> tbody > tr, > tr', table
)
685 .filter(':visible:odd').filter('.odd')
686 .removeClass('odd').addClass('even')
688 .filter(':visible:even').filter('.even')
689 .removeClass('even').addClass('odd');