Update Composer, update everything

This commit is contained in:
Oliver Davies 2018-11-23 12:29:20 +00:00
parent ea3e94409f
commit dda5c284b6
19527 changed files with 1135420 additions and 351004 deletions

View file

@ -1,41 +0,0 @@
/**
* @file
* Jquery plugin to find common ancestor.
*
* @see http://stackoverflow.com/questions/3217147/jquery-first-parent-containing-all-children
*/
(function ($) {
'use strict';
jQuery.fn.commonAncestor = function() {
var parents = [];
var minlen = Infinity;
$(this).each(function() {
var curparents = $(this).parents();
parents.push(curparents);
minlen = Math.min(minlen, curparents.length);
});
for (var i in parents) {
parents[i] = parents[i].slice(parents[i].length - minlen);
}
// Iterate until equality is found
for (var i = 0; i < parents[0].length; i++) {
var equal = true;
for (var j in parents) {
if (parents[j][i] != parents[0][i]) {
equal = false;
break;
}
}
if (equal) return $(parents[0][i]);
}
return $([]);
}
})(jQuery);

View file

@ -0,0 +1,26 @@
/**
* @file
* Dropbutton feature.
*/
(function ($, Drupal) {
'use strict';
// Make sure that dropButton behavior exists.
if (!Drupal.behaviors.dropButton) {
return;
}
/**
* Wrap Drupal's dropbutton behavior so that the dropbutton widget is only visible after it is initialized.
*/
var dropButton = Drupal.behaviors.dropButton;
Drupal.behaviors.dropButton = {
attach: function (context, settings) {
dropButton.attach(context, settings);
$(context).find('.webform-dropbutton .dropbutton-wrapper').once('webform-dropbutton').css('visibility', 'visible');
}
};
})(jQuery, Drupal);

View file

@ -1,12 +1,136 @@
/**
* @file
* Javascript behaviors for admin pages.
* JavaScript behaviors for admin pages.
*/
(function ($, Drupal) {
(function ($, Drupal, debounce) {
'use strict';
/**
* Filters the webform element list by a text input search string.
*
* The text input will have the selector `input.webform-form-filter-text`.
*
* The target element to do searching in will be in the selector
* `input.webform-form-filter-text[data-element]`
*
* The text source where the text should be found will have the selector
* `.webform-form-filter-text-source`
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the behavior for the webform element filtering.
*/
Drupal.behaviors.webformFilterByText = {
attach: function (context, settings) {
$('input.webform-form-filter-text', context).once('webform-form-filter-text').each(function () {
var $input = $(this);
var $table = $($input.data('element'));
var $summary = $($input.data('summary'));
var $noResults = $($input.data('no-results'));
var $details = $table.closest('details');
var sourceSelector = $input.data('source') || '.webform-form-filter-text-source';
var parentSelector = $input.data('parent') || 'tr';
var $filterRows;
var hasDetails = $details.length;
var totalItems;
var args = {
'@item': $input.data('item-single') || Drupal.t('item'),
'@items': $input.data('item-plural') || Drupal.t('items'),
'@total': null
};
if ($table.length) {
$filterRows = $table.find(sourceSelector);
$input
.attr('autocomplete', 'off')
.on('keyup', debounce(filterElementList, 200))
.keyup();
// Make sure the filter input is always focused.
setTimeout(function () {$input.focus();});
}
/**
* Filters the webform element list.
*
* @param {jQuery.Event} e
* The jQuery event for the keyup event that triggered the filter.
*/
function filterElementList(e) {
var query = $(e.target).val().toLowerCase();
// Filter if the length of the query is at least 2 characters.
if (query.length >= 2) {
// Reset count.
totalItems = 0;
if ($details.length) {
$details.hide();
}
$filterRows.each(toggleEntry);
// Announce filter changes.
// @see Drupal.behaviors.blockFilterByText
Drupal.announce(Drupal.formatPlural(
totalItems,
'1 @item is available in the modified list.',
'@total @items are available in the modified list.',
args
));
}
else {
totalItems = $filterRows.length;
$filterRows.each(function (index) {
$(this).closest(parentSelector).show();
if ($details.length) {
$details.show();
}
});
}
// Set total.
args['@total'] = totalItems;
// Hide/show no results.
$noResults[totalItems ? 'hide' : 'show']();
// Update summary.
if ($summary.length) {
$summary.html(Drupal.formatPlural(
totalItems,
'1 @item',
'@total @items',
args
));
}
/**
* Shows or hides the webform element entry based on the query.
*
* @param {number} index
* The index in the loop, as provided by `jQuery.each`
* @param {HTMLElement} label
* The label of the webform.
*/
function toggleEntry(index, label) {
var $label = $(label);
var $row = $label.closest(parentSelector);
var textMatch = $label.text().toLowerCase().indexOf(query) !== -1;
$row.toggle(textMatch);
if (textMatch) {
totalItems++;
if (hasDetails) {
$row.closest('details').show();
}
}
}
}
});
}
};
/**
* Filter webform autocomplete handler.
*
@ -42,8 +166,12 @@
attach: function (context) {
// Only attach the click event handler to the entire table and determine
// which row triggers the event.
$('.webform-results__table', context).once('webform-results-table').click(function (event) {
if (event.target.tagName == 'A' || event.target.tagName == 'BUTTON') {
$('.webform-results-table', context).once('webform-results-table').click(function (event) {
if (event.target.tagName === 'A' || event.target.tagName === 'BUTTON') {
return true;
}
if ($(event.target).parents('a[href]').length) {
return true;
}
@ -58,4 +186,4 @@
}
};
})(jQuery, Drupal);
})(jQuery, Drupal, Drupal.debounce);

View file

@ -0,0 +1,19 @@
/**
* @file
* Dropbutton feature.
*/
(function ($, Drupal) {
'use strict';
// Make sure that dropButton behavior exists.
if (!Drupal.behaviors.tableDrag) {
return;
}
$(function () {
$('head').append('<style type="text/css">.webform-tabledrag-hide {display: table-cell;}</style>');
});
})(jQuery, Drupal);

View file

@ -1,12 +1,206 @@
/**
* @file
* Javascript behaviors for AJAX.
* JavaScript behaviors for Ajax.
*/
(function ($, Drupal) {
'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);
/**
* 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')
.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').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).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).focus();
// Scroll to elements that are not visible.
if (!isScrolledIntoView($element)) {
$('html, body').animate({scrollTop: $element.offset().top - Drupal.webform.ajax.scrollTopOffset}, 500);
}
}
else {
// Focus main content.
$('#main-content').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.
*
@ -17,7 +211,7 @@
* @param {string} response.selector
* Selector to use.
*
* @see Drupal.AjaxCommands.prototype.webformScrollTop
* @see Drupal.AjaxCommands.prototype.viewScrollTop
*/
Drupal.AjaxCommands.prototype.webformScrollTop = function (ajax, response) {
// Scroll to the top of the view. This will allow users
@ -32,10 +226,141 @@
while ($(scrollTarget).scrollTop() === 0 && $(scrollTarget).parent()) {
scrollTarget = $(scrollTarget).parent();
}
// Only scroll upward.
if (offset.top - 10 < $(scrollTarget).scrollTop()) {
$(scrollTarget).animate({scrollTop: (offset.top - 10)}, 500);
if (response.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.ajax.scrollTopOffset < $(scrollTarget).scrollTop()) {
$(scrollTarget).animate({scrollTop: (offset.top - Drupal.webform.ajax.scrollTopOffset)}, 500);
}
}
// 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').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;
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').click();
}
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 audio UAs to read the supplied text.
*
* @param {Drupal.Ajax} [ajax]
* A {@link Drupal.ajax} object.
* @param {object} response
* Ajax response.
* @param {string} response.text
* A string to be read by the UA.
* @param {string} [response.priority='polite']
* A string to indicate the priority of the message. Can be either
* 'polite' or 'assertive'.
*
* @see Drupal.announce
*/
Drupal.AjaxCommands.prototype.webformAnnounce = function (ajax, response) {
// Delay the announcement.
setTimeout(function () {Drupal.announce(response.text, response.priority);}, 200);
};
/** ********************************************************************** **/
// Helper functions.
/** ********************************************************************** **/
/**
* 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
*/
function isScrolledIntoView(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

@ -34,7 +34,7 @@
* A string with the summary.
*/
function selectSummary(context) {
return $(context).find("#edit-visibility-webform-webforms option:selected").map(function(){ return this.text }).get().join(", ") || Drupal.t('Not restricted');
return $(context).find('#edit-visibility-webform-webforms option:selected').map(function () { return this.text; }).get().join(', ') || Drupal.t('Not restricted');
}
$('[data-drupal-selector="edit-visibility-webform"]').drupalSetSummary(selectSummary);

View file

@ -0,0 +1,50 @@
/**
* @file
* JavaScript behaviors for confirmation modal.
*/
(function ($, Drupal) {
'use strict';
// @see http://api.jqueryui.com/dialog/
Drupal.webform = Drupal.webform || {};
Drupal.webform.confirmationModal = Drupal.webform.confirmationModal || {};
Drupal.webform.confirmationModal.options = Drupal.webform.confirmationModal.options || {};
/**
* Display confirmation message in a modal.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformConfirmationModal = {
attach: function (context) {
$('.js-webform-confirmation-modal', context).once('webform-confirmation-modal').each(function () {
var $element = $(this);
var $dialog = $element.find('.webform-confirmation-modal--content');
var options = {
dialogClass: 'webform-confirmation-modal',
minWidth: 600,
resizable: false,
title: $element.find('.webform-confirmation-modal--title').text(),
close: function (event) {
Drupal.dialog(event.target).close();
Drupal.detachBehaviors(event.target, null, 'unload');
$(event.target).remove();
}
};
options = $.extend(options, Drupal.webform.confirmationModal.options);
var dialog = Drupal.dialog($dialog, options);
// Use setTimeout to prevent dialog.position.js
// Uncaught TypeError: Cannot read property 'settings' of undefined
setTimeout(function () {dialog.showModal();}, 1);
});
}
};
})(jQuery, Drupal);

View file

@ -0,0 +1,25 @@
/**
* @file
* JavaScript behavior to remove destination from contextual links.
*/
(function ($) {
'use strict';
// Bind click event to all .contextual links which are
// dynamically inserted via Ajax.
// @see webform_contextual_links_view_alter()
// @see Drupal.behaviors.contextual
$(document).on('click', '.contextual', function () {
$(this).find('a.webform-contextual').once('webform-contextual').each(function () {
this.href = this.href.split('?')[0];
// Add ?_webform_test={webform} to the current page's URL.
if (/webform\/([^/]+)\/test/.test(this.href)) {
this.href = window.location.pathname + '?_webform_test=' + RegExp.$1;
}
});
});
})(jQuery);

View file

@ -1,19 +1,75 @@
/**
* @file
* Javascript behaviors to fix dialogs.
* JavaScript behaviors for webform dialogs.
*/
(function ($, Drupal) {
(function ($, Drupal, drupalSettings) {
'use strict';
// @see http://stackoverflow.com/questions/20533487/how-to-ensure-that-ckeditor-has-focus-when-displayed-inside-of-jquery-ui-dialog
var _allowInteraction = $.ui.dialog.prototype._allowInteraction;
$.ui.dialog.prototype._allowInteraction = function (event) {
if ($(event.target).closest('.cke_dialog').length) {
return true;
// @see http://api.jqueryui.com/dialog/
Drupal.webform = Drupal.webform || {};
Drupal.webform.dialog = Drupal.webform.dialog || {};
Drupal.webform.dialog.options = Drupal.webform.dialog.options || {};
/**
* Open webform dialog using preset options.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformDialog = {
attach: function (context) {
$('a.webform-dialog', context).once('webform-dialog').each(function () {
var $a = $(this);
// Get default options.
var options = $.extend({}, Drupal.webform.dialog.options);
// Get preset dialog options.
if ($a.attr('class').match(/webform-dialog-([a-z0-9_]+)/)) {
var dialogOptionsName = RegExp.$1;
if (drupalSettings.webform.dialog.options[dialogOptionsName]) {
options = drupalSettings.webform.dialog.options[dialogOptionsName];
// Unset title.
delete options.title;
}
}
// Get custom dialog options.
if ($(this).data('dialog-options')) {
$.extend(options, $(this).data('dialog-options'));
}
var href = $a.attr('href');
// Replace ENTITY_TYPE and ENTITY_ID placeholders and update the href.
// @see webform_page_attachments()
if (href.indexOf('?source_entity_type=ENTITY_TYPE&source_entity_id=ENTITY_ID') !== -1) {
if (drupalSettings.webform.dialog.entity_type && drupalSettings.webform.dialog.entity_id) {
href = href.replace('ENTITY_TYPE', encodeURIComponent(drupalSettings.webform.dialog.entity_type));
href = href.replace('ENTITY_ID', encodeURIComponent(drupalSettings.webform.dialog.entity_id));
}
else {
href = href.replace('?source_entity_type=ENTITY_TYPE&source_entity_id=ENTITY_ID', '');
}
$a.attr('href', href);
}
// Append _webform_dialog=1 to href to trigger Ajax support.
// @see \Drupal\webform\WebformSubmissionForm::setEntity
href += (href.indexOf('?') === -1 ? '?' : '&') + '_webform_dialog=1';
var element_settings = {};
element_settings.progress = {type: 'fullscreen'};
element_settings.url = href;
element_settings.event = 'click';
element_settings.dialogType = $a.data('dialog-type') || 'modal';
element_settings.dialog = options;
element_settings.element = this;
Drupal.ajax(element_settings);
});
}
return _allowInteraction.apply(this, arguments);
};
})(jQuery, Drupal);
})(jQuery, Drupal, drupalSettings);

View file

@ -1,12 +1,21 @@
/**
* @file
* Javascript behaviors for jQuery UI buttons element integration.
* JavaScript behaviors for jQuery UI buttons (checkboxradio) element integration.
*/
(function ($, Drupal) {
'use strict';
Drupal.webform = Drupal.webform || {};
Drupal.webform.buttons = Drupal.webform.buttons || {};
Drupal.webform.buttons.selector = Drupal.webform.buttons.selector || [
// Applies to Classy, Bartik, and Seven themes.
'.js-webform-buttons .form-radios',
// Applies to Stable and Bootstrap themes.
'.js-webform-buttons .js-form-type-radio'
].join(',');
/**
* Create jQuery UI buttons element.
*
@ -14,19 +23,33 @@
*/
Drupal.behaviors.webformButtons = {
attach: function (context) {
$(context).find('.js-webform-buttons [type="radio"]').commonAncestor().once('webform-buttons').each(function () {
var $input = $(this);
// Remove all div and classes around radios and labels.
$input.html($input.find('input[type="radio"], label').removeClass());
// Create buttonset.
$input.buttonset();
// Disable buttonset.
$input.buttonset('option', 'disabled', $input.find('input[type="radio"]:disabled').length);
$(context).find(Drupal.webform.buttons.selector).once('webform-buttons').each(function () {
var $buttons = $(this);
// Turn buttonset off/on when the input is disabled/enabled.
// Remove classes around radios and labels and move to main element.
$buttons.find('input[type="radio"], label').each(function () {
$buttons.append($(this).removeAttr('class'));
});
// Remove all empty div wrappers.
$buttons.find('div').remove();
// Must reset $buttons since the contents have changed.
$buttons = $(this);
// Get radios.
var $input = $buttons.find('input[type="radio"]');
// Create checkboxradio.
$input.checkboxradio({icon: false});
// Disable checkboxradio.
$input.checkboxradio('option', 'disabled', $input.is(':disabled'));
// Turn checkboxradio off/on when the input is disabled/enabled.
// @see webform.states.js
$input.on('webform:disabled', function () {
$input.buttonset('option', 'disabled', $input.find('input[type="radio"]:disabled').length);
$input.checkboxradio('option', 'disabled', $input.is(':disabled'));
});
});
}

View file

@ -0,0 +1,76 @@
/**
* @file
* JavaScript behaviors for Chosen integration.
*/
(function ($, Drupal) {
'use strict';
// @see https://harvesthq.github.io/chosen/options.html
Drupal.webform = Drupal.webform || {};
Drupal.webform.chosen = Drupal.webform.chosen || {};
Drupal.webform.chosen.options = Drupal.webform.chosen.options || {};
/**
* Initialize Chosen support.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformChosen = {
attach: function (context) {
if (!$.fn.chosen) {
return;
}
// Add HTML5 required attribute support.
// Checking for $oldChosen to prevent duplicate workarounds from
// being applied.
// @see https://github.com/harvesthq/chosen/issues/515
if (!$.fn.oldChosen) {
$.fn.oldChosen = $.fn.chosen;
$.fn.chosen = function (options) {
var select = $(this);
var is_creating_chosen = !!options;
if (is_creating_chosen && select.css('position') === 'absolute') {
select.removeAttr('style');
}
var ret = select.oldChosen(options);
if (is_creating_chosen && select.css('display') === 'none') {
select.attr('style', 'display:visible; position:absolute; width:0px; height: 0px; clip:rect(0,0,0,0)');
select.attr('tabindex', -1);
}
return ret;
};
}
$(context)
.find('select.js-webform-chosen, .js-webform-chosen select')
.once('webform-chosen')
.each(function () {
var $select = $(this);
// Check for .chosen-enable to prevent the chosen.module and
// webform.module from both initializing the chosen select element.
if ($select.hasClass('chosen-enable')) {
return;
}
var options = $.extend({width: '100%'}, Drupal.webform.chosen.options);
if ($select.data('placeholder')) {
if ($select.prop('multiple')) {
options.placeholder_text_multiple = $select.data('placeholder');
}
else {
// Clear option value so that placeholder is displayed.
$select.find('option[value=""]').html('');
// Allow single option to be deselected.
options.allow_single_deselect = true;
}
}
$select.chosen(options);
});
}
};
})(jQuery, Drupal);

View file

@ -1,12 +1,17 @@
/**
* @file
* Javascript behaviors for CodeMirror integration.
* JavaScript behaviors for CodeMirror integration.
*/
(function ($, Drupal) {
'use strict';
// @see http://codemirror.net/doc/manual.html#config
Drupal.webform = Drupal.webform || {};
Drupal.webform.codeMirror = Drupal.webform.codeMirror || {};
Drupal.webform.codeMirror.options = Drupal.webform.codeMirror.options || {};
/**
* Initialize CodeMirror editor.
*
@ -14,6 +19,9 @@
*/
Drupal.behaviors.webformCodeMirror = {
attach: function (context) {
if (!window.CodeMirror) {
return;
}
// Webform CodeMirror editor.
$(context).find('textarea.js-webform-codemirror').once('webform-codemirror').each(function () {
@ -27,19 +35,37 @@
// https://github.com/marijnh/CodeMirror-old/issues/59
$(this).removeAttr('required');
var editor = CodeMirror.fromTextArea(this, {
var options = $.extend({
mode: $(this).attr('data-webform-codemirror-mode'),
lineNumbers: true,
lineWrapping: true,
viewportMargin: Infinity,
readOnly: $(this).prop('readonly') ? true : false,
// Setting for using spaces instead of tabs - https://github.com/codemirror/CodeMirror/issues/988
readOnly: ($(this).prop('readonly') || $(this).prop('disabled')) ? true : false,
extraKeys: {
// Setting for using spaces instead of tabs - https://github.com/codemirror/CodeMirror/issues/988
Tab: function (cm) {
var spaces = Array(cm.getOption('indentUnit') + 1).join(' ');
cm.replaceSelection(spaces, 'end', '+element');
},
// On 'Escape' move to the next tabbable input.
// @see http://bgrins.github.io/codemirror-accessible/
Esc: function (cm) {
// Must show and then textarea so that we can determine
// its tabindex.
var textarea = $(cm.getTextArea());
$(textarea).show().addClass('visually-hidden');
var $tabbable = $(':tabbable');
var tabindex = $tabbable.index(textarea);
$(textarea).hide().removeClass('visually-hidden');
// Tabindex + 2 accounts for the CodeMirror's iframe.
$tabbable.eq(tabindex + 2).focus();
}
}
});
}, Drupal.webform.codeMirror.options);
var editor = CodeMirror.fromTextArea(this, options);
// Now, close details.
$details.removeAttr('open');
@ -59,45 +85,38 @@
// Set CodeMirror to be readonly when the textarea is disabled.
// @see webform.states.js
$input.on('webform:disabled', function () {
editor.setOption("readOnly", $input.is(':disabled'));
editor.setOption('readOnly', $input.is(':disabled'));
});
// Delay refreshing CodeMirror for 10 millisecond while the dialog is
// still being rendered.
// @see http://stackoverflow.com/questions/8349571/codemirror-editor-is-not-loading-content-until-clicked
setTimeout(function () {
// Show tab panel and open details.
var $tabPanel = $input.parents('.ui-tabs-panel:hidden');
var $details = $input.parents('details:not([open])');
if (!$tabPanel.length && $details.length) {
return;
}
$tabPanel.show();
$details.attr('open', 'open');
editor.refresh();
// Hide tab panel and close details.
$tabPanel.hide();
$details.removeAttr('open');
}, 10);
});
// Webform CodeMirror syntax coloring.
$(context).find('.js-webform-codemirror-runmode').once('webform-codemirror-runmode').each(function () {
// Mode Runner - http://codemirror.net/demo/runmode.html
CodeMirror.runMode($(this).addClass('cm-s-default').html(), $(this).attr('data-webform-codemirror-mode'), this);
CodeMirror.runMode($(this).addClass('cm-s-default').text(), $(this).attr('data-webform-codemirror-mode'), this);
});
}
};
// Workaround: When a dialog opens we need to reference all CodeMirror
// editors to make sure they are properly initialized and sized.
$(window).on('dialog:aftercreate', function (dialog, $element, settings) {
// Delay refreshing CodeMirror for 10 millisecond while the dialog is
// still being rendered.
// @see http://stackoverflow.com/questions/8349571/codemirror-editor-is-not-loading-content-until-clicked
setTimeout(function () {
$('.CodeMirror').each(function (index, $element) {
var $details = $(this).parents('details:not([open])');
$details.attr('open', 'open');
$element.CodeMirror.refresh();
// Now, close details.
$details.removeAttr('open');
});
}, 10);
});
// On state:visible refresh CodeMirror elements.
$(document).on('state:visible', function (event) {
var $element = $(event.target);
if ($element.hasClass('js-webform-codemirror')) {
$element.parent().find('.CodeMirror').each(function (index, $element) {
$element.CodeMirror.refresh();
});
}
});
})(jQuery, Drupal);

View file

@ -1,6 +1,6 @@
/**
* @file
* Javascript behaviors for color element integration.
* JavaScript behaviors for color element integration.
*/
(function ($, Drupal) {
@ -24,11 +24,21 @@
.removeClass('form-color-large');
}
else {
// Display color input's output to the end user.
var $output = $('<input class="form-color-output ' + $element.attr('class') + ' js-webform-element-mask" data-inputmask-mask="\\#######" />').inputmask();
// Display color input's output w/ visually-hidden label to
// the end user.
var $output = $('<input class="form-color-output ' + $element.attr('class') + ' js-webform-input-mask" data-inputmask-mask="\\#######" />').uniqueId();
var $label = $element.parent('.js-form-type-color').find('label').clone();
$label.attr({
for: $output.attr('id'),
class: 'visually-hidden'
});
if ($.fn.inputmask) {
$output.inputmask();
}
$output[0].value = $element[0].value;
$element
.after($output)
.after($label)
.css({float: 'left'});
// Sync $element and $output.
@ -39,7 +49,7 @@
$element[0].value = $output[0].value;
});
}
})
});
}
};

View file

@ -0,0 +1,42 @@
/**
* @file
* JavaScript behaviors for composite element builder.
*/
(function ($, Drupal) {
'use strict';
/**
* Initialize composite element builder.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformElementComposite = {
attach: function (context) {
$('[data-composite-types]').once('webform-composite-types').each(function () {
var $element = $(this);
var $type = $element.closest('tr').find('.js-webform-composite-type');
var types = $element.attr('data-composite-types').split(',');
var required = $element.attr('data-composite-required');
$type.on('change', function () {
if ($.inArray($(this).val(), types) === -1) {
$element.hide();
if (required) {
$element.removeAttr('required aria-required');
}
}
else {
$element.show();
if (required) {
$element.attr({'required': 'required', 'aria-required': 'true'});
}
}
}).change();
});
}
};
})(jQuery, Drupal);

View file

@ -0,0 +1,64 @@
/**
* @file
* JavaScript behaviors for computed elements.
*/
(function ($, Drupal, debounce) {
'use strict';
Drupal.webform = Drupal.webform || {};
Drupal.webform.computed = Drupal.webform.computed || {};
Drupal.webform.computed.delay = Drupal.webform.computed.delay || 500;
/**
* Initialize computed elements.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformComputed = {
attach: function (context) {
$(context).find('.js-webform-computed').once('webform-computed').each(function () {
// Get computed element and parent form.
var $element = $(this);
var $form = $element.closest('form');
// Get elements that are used by the computed element.
var elementKeys = $(this).data('webform-element-keys').split(',');
if (!elementKeys) {
return;
}
// Add event handler to elements that are used by the computed element.
$.each(elementKeys, function (i, key) {
$form.find(':input[name^="' + key + '"]')
.on('keyup change', debounce(triggerUpdate, Drupal.webform.computed.delay));
});
// Initialize computed element update which refreshes the displayed
// value and accounts for any changes to the #default_value for a
// computed element.
triggerUpdate();
function triggerUpdate() {
// Prevent duplicate computations.
// @see Drupal.behaviors.formSingleSubmit
var formValues = $form.find('input[name!=form_build_id]').serialize();
var previousValues = $element.attr('data-webform-computed-last');
if (previousValues === formValues) {
return;
}
$element.attr('data-webform-computed-last', formValues);
// Add loading class to computed wrapper.
$element.find('.js-webform-computed-wrapper')
.addClass('webform-computed-loading');
// Trigger computation.
$element.find('.js-form-submit').mousedown();
}
});
}
};
})(jQuery, Drupal, Drupal.debounce);

View file

@ -1,12 +1,17 @@
/**
* @file
* Javascript behaviors for jQuery Word and Counter Counter integration.
* JavaScript behaviors for jQuery Text Counter integration.
*/
(function ($, Drupal) {
'use strict';
// @see https://github.com/ractoon/jQuery-Text-Counter#options
Drupal.webform = Drupal.webform || {};
Drupal.webform.counter = Drupal.webform.counter || {};
Drupal.webform.counter.options = Drupal.webform.counter.options || {};
/**
* Initialize text field and textarea word and character counter.
*
@ -14,23 +19,39 @@
*/
Drupal.behaviors.webformCounter = {
attach: function (context) {
if (!$.fn.textcounter) {
return;
}
$(context).find('.js-webform-counter').once('webform-counter').each(function () {
var options = {
goal: $(this).attr('data-counter-limit'),
msg: $(this).attr('data-counter-message')
type: $(this).data('counter-type'),
max: $(this).data('counter-maximum'),
min: $(this).data('counter-minimum') || 0,
counterText: $(this).data('counter-minimum-message'),
countDownText: $(this).data('counter-maximum-message'),
inputErrorClass: 'webform-counter-warning',
counterErrorClass: 'webform-counter-warning',
countSpaces: true,
stopInputAtMaximum: false,
// Don't display min/max message since server-side validation will
// display these messages.
minimumErrorText: '',
maximumErrorText: ''
};
// Only word type can be defined, otherwise the counter defaults to
// character counting.
if ($(this).attr('data-counter-type') == 'word') {
options.type = 'word';
options.countDown = (options.max) ? true : false;
if (!options.counterText) {
options.counterText = (options.type === 'word') ? Drupal.t('%d word(s) entered') : Drupal.t('%d characters(s) entered');
}
if (!options.countDownText) {
options.countDownText = (options.type === 'word') ? Drupal.t('%d word(s) remaining') : Drupal.t('%d characters(s) remaining');
}
// Set the target to a div that is appended to end of the input's parent container.
options.target = $('<div></div>');
$(this).parent().append(options.target);
options = $.extend(options, Drupal.webform.counter.options);
$(this).counter(options);
})
$(this).textcounter(options);
});
}
};

View file

@ -7,6 +7,11 @@
'use strict';
// @see http://api.jqueryui.com/datepicker/
Drupal.webform = Drupal.webform || {};
Drupal.webform.datePicker = Drupal.webform.datePicker || {};
Drupal.webform.datePicker.options = Drupal.webform.datePicker.options || {};
/**
* Attach datepicker fallback on date elements.
*
@ -22,48 +27,70 @@
Drupal.behaviors.date = {
attach: function (context, settings) {
var $context = $(context);
// Skip if date are supported by the browser.
if (Modernizr.inputtypes.date === true) {
return;
}
$context.find('input[data-drupal-date-format]').once('datePicker').each(function () {
var $input = $(this);
var datepickerSettings = {};
// Skip if date inputs are supported by the browser and input is not a text field.
// @see \Drupal\webform\Element\WebformDatetime
if (window.Modernizr && Modernizr.inputtypes.date === true && $input.attr('type') !== 'text') {
return;
}
var options = $.extend({
changeMonth: true,
changeYear: true
}, Drupal.webform.datePicker.options);
var dateFormat = $input.data('drupalDateFormat');
// The date format is saved in PHP style, we need to convert to jQuery
// datepicker.
// @see http://stackoverflow.com/questions/16702398/convert-a-php-date-format-to-a-jqueryui-datepicker-date-format
datepickerSettings.dateFormat = dateFormat
// Year.
.replace('Y', 'yy')
// @see http://php.net/manual/en/function.date.php
options.dateFormat = dateFormat
// Year.
.replace('Y', 'yy') // A full numeric representation of a year, 4 digits (1999 or 2003)
.replace('y', 'y') // A two digit representation of a year (99 or 03)
// Month.
.replace('F', 'MM')
.replace('m', 'mm')
.replace('n', 'm')
// Date.
.replace('d', 'dd');
.replace('F', 'MM') // A full textual representation of a month, such as January or March (January through December)
.replace('m', 'mm') // Numeric representation of a month, with leading zeros (01 through 12)
.replace('M', 'M') // A short textual representation of a month, three letters (Jan through Dec)
.replace('n', 'm') // Numeric representation of a month, without leading zeros (1 through 12)
// Day.
.replace('d', 'dd') // Day of the month, 2 digits with leading zeros (01 to 31)
.replace('D', 'D') // A textual representation of a day, three letters (Mon through Sun)
.replace('j', 'd') // Day of the month without leading zeros (1 to 31)
.replace('l', 'DD'); // A full textual representation of the day of the week (Sunday through Saturday)
// Add min and max date if set on the input.
if ($input.attr('min')) {
datepickerSettings.minDate = $.datepicker.formatDate(datepickerSettings.dateFormat, $.datepicker.parseDate('yy-mm-dd', $input.attr('min')));
options.minDate = $input.attr('min');
}
if ($input.attr('max')) {
datepickerSettings.maxDate = $.datepicker.formatDate(datepickerSettings.dateFormat, $.datepicker.parseDate('yy-mm-dd', $input.attr('max')));
options.maxDate = $input.attr('max');
}
// Format default value.
if ($input.val()) {
$input.val($.datepicker.formatDate(datepickerSettings.dateFormat, $.datepicker.parseDate('yy-mm-dd', $input.val())));
// Add min/max year to data range.
if (!options.yearRange && $input.data('min-year') && $input.data('max-year')) {
options.yearRange = $input.data('min-year') + ':' + $input.attr('data-max-year');
}
$input.datepicker(datepickerSettings);
// First day of the week.
options.firstDay = settings.webform.dateFirstDay;
$input.datepicker(options);
});
}
// Issue #2983363: Datepicker is being detached when multiple files are
// uploaded.
/*
},
detach: function (context, settings, trigger) {
if (trigger === 'unload') {
$(context).find('input[data-drupal-date-format]').findOnce('datePicker').datepicker('destroy');
}
}
*/
};
})(jQuery, Modernizr, Drupal);

View file

@ -1,6 +1,6 @@
/**
* @file
* Javascript behaviors for details element.
* JavaScript behaviors for details element.
*/
(function ($, Drupal) {
@ -22,12 +22,18 @@
$('details > summary', context).once('webform-details-summary-save').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;
var open = ($details.attr('open') !== 'open') ? '1' : '0';
localStorage.setItem(name, open);
});
@ -45,7 +51,7 @@
return;
}
if (open == 1) {
if (open === '1') {
$details.attr('open', 'open');
}
else {
@ -59,10 +65,10 @@
/**
* Get the name used to store the state of details element.
*
* @param $details
* @param {jQuery} $details
* A details element.
*
* @returns string
* @return {string}
* The name used to store the state of details element.
*/
Drupal.webformDetailsSaveGetName = function ($details) {
@ -91,12 +97,12 @@
return '';
}
// ISSUE: When Drupal renders a webform in a modal dialog it appends a unique
// identifier to webform ids and details ids. (ie my-form--FeSFISegTUI)
// 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

@ -1,12 +1,16 @@
/**
* @file
* Javascript behaviors for details element.
* 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.
*
@ -16,15 +20,27 @@
attach: function (context) {
$('.js-webform-details-toggle', context).once('webform-details-toggle').each(function () {
var $form = $(this);
var $details = $form.find('details');
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';
var $details = $form.find('details').filter(function () {
// @todo Figure out how to optimize the below code.
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;
}
// Add toggle state link to first details element.
$details.first().before($('<button type="button" class="link webform-details-toggle-state"></button>')
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) {
var open;
@ -40,7 +56,7 @@
// Set the saved states for all the details elements.
// @see webform.element.details.save.js
if (Drupal.webformDetailsSaveGetName) {
if (!Drupal.webformDetailsSaveGetName) {
$form.find('details').each(function () {
var name = Drupal.webformDetailsSaveGetName($(this));
if (name) {
@ -50,8 +66,16 @@
}
})
.wrap('<div class="webform-details-toggle-state-wrapper"></div>')
.parent()
);
.parent();
if ($tabs.length) {
// Add toggle state before the tabs.
$tabs.find('.item-list:first-child').before($toggle);
}
else {
// Add toggle state link to first details element.
$details.eq(0).before($toggle);
}
setDetailsToggleLabel($form);
});
@ -61,25 +85,30 @@
/**
* Determine if a webform's details are all opened.
*
* @param $form
* @param {jQuery} $form
* A webform.
*
* @returns {boolean}
* @return {boolean}
* TRUE if a webform's details are all opened.
*/
function isFormDetailsOpen($form) {
return ($form.find('details[open]').length == $form.find('details').length)
return ($form.find('details[open]').length === $form.find('details').length);
}
/**
* Set a webform's details toggle state widget label.
*
* @param $form
* @param {jQuery} $form
* A webform.
*/
function setDetailsToggleLabel($form) {
var label = (isFormDetailsOpen($form)) ? Drupal.t('Collapse all') : Drupal.t('Expand all');
var isOpen = 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,73 @@
/**
* @file
* JavaScript behaviors for element help text (tooltip).
*/
(function ($, Drupal) {
'use strict';
// @see http://api.jqueryui.com/tooltip/
Drupal.webform = Drupal.webform || {};
Drupal.webform.elementHelpIcon = Drupal.webform.elementHelpIcon || {};
Drupal.webform.elementHelpIcon.options = Drupal.webform.elementHelpIcon.options || {
position: {my: 'left+5 top+5', at: 'left bottom', collision: 'flipfit'},
tooltipClass: 'webform-element-help--tooltip',
// @see https://stackoverflow.com/questions/18231315/jquery-ui-tooltip-html-with-links
show: {delay: 100},
close: function (event, ui) {
ui.tooltip.hover(
function () {
$(this).stop(true).fadeTo(400, 1);
},
function () {
$(this).fadeOut('400', function () {
$(this).remove();
});
});
}
};
/**
* Element help icon.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformElementHelpIcon = {
attach: function (context) {
$(context).find('.webform-element-help').once('webform-element-help').each(function () {
var $link = $(this);
var options = $.extend({
// Use 'data-webform-help' attribute which can include HTML markup.
content: $link.attr('data-webform-help'),
items: '[data-webform-help]'
}, Drupal.webform.elementHelpIcon.options);
$link.tooltip(options)
.on('click', function (event) {
// Prevent click from toggling <label>s wrapped around help.
event.preventDefault();
});
// Help tooltips are generally placed with <label> tags.
// Screen readers are also reading the <label> and the
// 'aria-describedby' attribute.
// To prevent this issue we are removing the <label>'s 'for' attribute
// when the tooltip is focused.
var $label = $(this).parent('label');
var labelFor = $label.attr('for') || '';
if ($label.length && labelFor) {
$link
.on('focus', function () {
$label.removeAttr('for');
})
.on('blur', function () {
$label.attr('for', labelFor);
});
}
});
}
};
})(jQuery, Drupal);

View file

@ -1,12 +1,17 @@
/**
* @file
* Javascript behaviors for HTML editor integration.
* JavaScript behaviors for HTML editor integration.
*/
(function ($, Drupal, drupalSettings) {
'use strict';
// @see http://docs.ckeditor.com/#!/api/CKEDITOR.config
Drupal.webform = Drupal.webform || {};
Drupal.webform.htmlEditor = Drupal.webform.htmlEditor || {};
Drupal.webform.htmlEditor.options = Drupal.webform.htmlEditor.options || {};
/**
* Initialize HTML Editor.
*
@ -14,10 +19,40 @@
*/
Drupal.behaviors.webformHtmlEditor = {
attach: function (context) {
$(context).find('.js-form-type-webform-html-editor textarea').once('webform-html-editor').each(function () {
var allowedContent = drupalSettings['webform']['html_editor']['allowedContent'];
if (!window.CKEDITOR) {
return;
}
$(context).find('textarea.js-html-editor').once('webform-html-editor').each(function () {
var $textarea = $(this);
CKEDITOR.replace(this.id, {
var allowedContent = drupalSettings['webform']['html_editor']['allowedContent'];
// Load additional CKEditor plugins used by the Webform HTML editor.
// @see \Drupal\webform\Element\WebformHtmlEditor::preRenderWebformHtmlEditor
// @see \Drupal\webform\WebformLibrariesManager::initLibraries
var plugins = drupalSettings['webform']['html_editor']['plugins'];
// If requirejs is present don't use the codemirror plugin.
// @see Issue #2936147: ckeditor.codemirror plugin breaks admin textarea.
// @todo Remove the below code once this issue is resolved.
if (plugins.codemirror
&& drupalSettings.yamlEditor
&& drupalSettings.yamlEditor.source
&& drupalSettings.yamlEditor.source.indexOf('noconflict') !== -1) {
delete plugins.codemirror;
if ('console' in window) {
window.console.log('YAML Editor module is not compatible with the ckeditor.codemirror plugin. @see Issue #2936147: ckeditor.codemirror plugin breaks admin textarea.');
}
}
for (var plugin_name in plugins) {
if (plugins.hasOwnProperty(plugin_name)) {
CKEDITOR.plugins.addExternal(plugin_name, plugins[plugin_name]);
}
}
var options = {
// Turn off external config and styles.
customConfig: '',
stylesSet: false,
@ -26,26 +61,76 @@
// Use <br> tags instead of <p> tags.
enterMode: CKEDITOR.ENTER_BR,
shiftEnterMode: CKEDITOR.ENTER_BR,
// Set height, hide the status bar, and remove plugins.
// Set height.
height: '100px',
// Remove status bar.
resize_enabled: false,
removePlugins: 'elementspath,magicline',
// Toolbar settings.
format_tags: 'p;h2;h3;h4;h5;h6',
toolbar: [
{ name: 'styles', items: ['Format', 'Font', 'FontSize' ] },
{ name: 'basicstyles', items: [ 'Bold', 'Italic', 'Subscript', 'Superscript' ] },
{ name: 'insert', items: [ 'SpecialChar' ] },
{ name: 'colors', items: [ 'TextColor', 'BGColor' ] },
{ name: 'paragraph', items: [ 'NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'Blockquote'] },
{ name: 'links', items: [ 'Link', 'Unlink'] },
{ name: 'tools', items: [ 'Source', '-', 'Maximize' ] }
]
}).on('change', function (evt) {
// Save data onchange since AJAX dialogs don't execute webform.onsubmit.
$textarea.val(evt.editor.getData().trim());
});
})
// extraPlugins
extraPlugins: ''
};
// Add toolbar.
if (!options.toolbar) {
options.toolbar = [];
options.toolbar.push({name: 'styles', items: ['Format', 'Font', 'FontSize']});
options.toolbar.push({name: 'basicstyles', items: ['Bold', 'Italic', 'Subscript', 'Superscript']});
// Add IMCE image button.
if (CKEDITOR.plugins.get('imce')) {
CKEDITOR.config.ImceImageIcon = drupalSettings['webform']['html_editor']['ImceImageIcon'];
options.extraPlugins += (options.extraPlugins ? ',' : '') + 'imce';
options.toolbar.push({name: 'insert', items: ['ImceImage', 'SpecialChar']});
}
else {
options.toolbar.push({name: 'insert', items: ['SpecialChar']});
}
// Add link plugin.
if (plugins['link']) {
options.extraPlugins += (options.extraPlugins ? ',' : '') + 'link';
options.toolbar.push({name: 'links', items: ['Link', 'Unlink']});
}
options.toolbar.push({name: 'colors', items: ['TextColor', 'BGColor']});
options.toolbar.push({name: 'paragraph', items: ['NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'Blockquote']});
options.toolbar.push({name: 'tools', items: ['Source', '-', 'Maximize']});
}
// Add autogrow plugin.
if (plugins['autogrow']) {
options.extraPlugins += (options.extraPlugins ? ',' : '') + 'autogrow';
options.autoGrow_minHeight = 60;
options.autoGrow_maxHeight = 300;
}
// Add CodeMirror integration plugin.
if (plugins['codemirror']) {
options.extraPlugins += (options.extraPlugins ? ',' : '') + 'codemirror';
options.codemirror = {
mode: 'text/html'
};
}
options = $.extend(options, Drupal.webform.htmlEditor.options);
// Catch and suppress
// "Uncaught TypeError: Cannot read property 'getEditor' of undefined".
//
// Steps to reproduce this error.
// - Goto any form elements.
// - Edit an element.
// - Save the element.
try {
CKEDITOR.replace(this.id, options).on('change', function (evt) {
// Save data onchange since Ajax dialogs don't execute form.onsubmit.
$textarea.val(evt.editor.getData().trim());
});
}
catch (e) {
// Do nothing.
}
});
}
};

View file

@ -0,0 +1,110 @@
/**
* @file
* JavaScript behaviors for iCheck integration.
*/
(function ($, Drupal) {
'use strict';
// @see http://icheck.fronteed.com/#options
Drupal.webform = Drupal.webform || {};
Drupal.webform.iCheck = Drupal.webform.iCheck || {};
Drupal.webform.iCheck.options = Drupal.webform.iCheck.options || {};
/**
* Enhance checkboxes and radios using iCheck.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformICheck = {
attach: function (context) {
if (!$.fn.iCheck) {
return;
}
$('input[data-webform-icheck]', context).each(function () {
var $input = $(this);
var icheck = $input.attr('data-webform-icheck');
var options = $.extend({
checkboxClass: 'icheckbox_' + icheck,
radioClass: 'iradio_' + icheck
}, Drupal.webform.iCheck.options);
// The line skin requires that the label be added to the options.
// @see http://icheck.fronteed.com/#skin-line
if (icheck.indexOf('line') === 0) {
var $label = $input.parent().find('label[for="' + $input.attr('id') + '"]');
// Set insert with label text.
options.insert = '<div class="icheck_line-icon"></div>' + $label.text();
// Make sure checkbox is outside the label and then remove the label.
$label.insertAfter($input).remove();
}
$input.addClass('js-webform-icheck')
.iCheck(options)
// @see https://github.com/fronteed/iCheck/issues/244
.on('ifChecked', function (e) {
$(e.target).attr('checked', 'checked').change();
})
.on('ifUnchecked', function (e) {
$(e.target).removeAttr('checked').change();
});
});
}
};
/**
* Enhance table select checkall.
*
* ISSUE: Select all is not sync'd with checkboxes because iCheck overrides all existing event handlers.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformICheckTableSelectAll = {
attach: function (context) {
if (!$.fn.iCheck) {
return;
}
$('table[data-webform-icheck] th.select-all').bind('DOMNodeInserted', function () {
$(this).unbind('DOMNodeInserted');
$(this).find('input[type="checkbox"]').each(function () {
var icheck = $(this).closest('table[data-webform-icheck]').attr('data-webform-icheck');
var options = $.extend({
checkboxClass: 'icheckbox_' + icheck,
radioClass: 'iradio_' + icheck
}, Drupal.webform.iCheck.options);
$(this).iCheck(options);
})
.on('ifChanged', function () {
var _index = $(this).parents('th').index() + 1;
$(this).parents('thead').next('tbody').find('tr td:nth-child(' + _index + ') input')
.iCheck(!$(this).is(':checked') ? 'check' : 'uncheck')
.iCheck($(this).is(':checked') ? 'check' : 'uncheck');
});
});
}
};
/**
* Sync iCheck element when checkbox/radio is enabled/disabled via the #states.
*
* @see core/misc/states.js
*/
if ($.fn.iCheck) {
$(document).on('state:disabled', function (e) {
if ($(e.target).hasClass('.js-webform-icheck')) {
$(e.target).iCheck(e.value ? 'disable' : 'enable');
}
$(e.target).iCheck(e.value ? 'disable' : 'enable');
});
}
})(jQuery, Drupal);

View file

@ -0,0 +1,35 @@
/**
* @file
* JavaScript behaviors for webform_image_file modal.
*/
(function ($, Drupal) {
'use strict';
/**
* Display webform image file in a modal.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformImageFileModal = {
attach: function (context) {
$('.js-webform-image-file-modal', context).once('js-webform-image-file-modal').on('click', function () {
// http://stackoverflow.com/questions/11442712/get-width-height-of-remote-image-from-url
var img = new Image();
img.src = $(this).attr('href');
img.onload = function () {
$('<div><img src="' + this.src + '" style="display: block; margin: 0 auto" /></div>').dialog({
dialogClass: 'webform-image-file-modal-dialog',
width: this.width + 60,
height: this.height + 100,
resizable: false,
modal: true
}).dialog('open');
};
return false;
});
}
};
})(jQuery, Drupal);

View file

@ -0,0 +1,51 @@
/**
* @file
* JavaScript behaviors for input hiding.
*/
(function ($, Drupal) {
'use strict';
var isChrome = /Chrome/.test(window.navigator.userAgent) && /Google Inc/.test(window.navigator.vendor);
/**
* Initialize input hiding.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformInputHide = {
attach: function (context) {
// Apply chrome fix to prevent password input from being autofilled.
// @see https://stackoverflow.com/questions/15738259/disabling-chrome-autofill
if (isChrome) {
$(context).find('form:has(input.js-webform-input-hide)')
.once('webform-input-hide-chrome-workaround')
.each(function () {
$(this).prepend('<input style="display:none" type="text" name="chrome_autocomplete_username"/><input style="display:none" type="password" name="chrome_autocomplete_password"/>');
});
}
// Convert text based inputs to password input on blur.
$(context).find('input.js-webform-input-hide')
.once('webform-input-hide')
.each(function () {
var type = this.type;
// Initialize input hiding.
this.type = 'password';
// Attach blur and focus event handlers.
$(this)
.on('blur', function () {
this.type = 'password';
$(this).attr('autocomplete', 'off');
})
.on('focus', function () {
this.type = type;
$(this).removeAttr('autocomplete');
});
});
}
};
})(jQuery, Drupal);

View file

@ -1,6 +1,6 @@
/**
* @file
* Javascript behaviors for jquery.inputmask integration.
* JavaScript behaviors for jquery.inputmask integration.
*/
(function ($, Drupal) {
@ -12,9 +12,13 @@
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformElementMask = {
Drupal.behaviors.webformInputMask = {
attach: function (context) {
$(context).find('input.js-webform-element-mask').once('webform-element-mask').inputmask();
if (!$.fn.inputmask) {
return;
}
$(context).find('input.js-webform-input-mask').once('webform-input-mask').inputmask();
}
};

View file

@ -0,0 +1,67 @@
/**
* @file
* JavaScript behaviors for Geocomplete location integration.
*/
(function ($, Drupal) {
'use strict';
// @see https://ubilabs.github.io/geocomplete/
// @see https://developers.google.com/maps/documentation/javascript/reference?csw=1#MapOptions
Drupal.webform = Drupal.webform || {};
Drupal.webform.locationGeocomplete = Drupal.webform.locationGeocomplete || {};
Drupal.webform.locationGeocomplete.options = Drupal.webform.locationGeocomplete.options || {};
/**
* Initialize location geocomplete.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformLocationGeocomplete = {
attach: function (context) {
if (!$.fn.geocomplete) {
return;
}
$(context).find('.js-webform-type-webform-location-geocomplete').once('webform-location-geocomplete').each(function () {
var $element = $(this);
var $input = $element.find('.webform-location-geocomplete');
// Display a map.
var $map = null;
if ($input.attr('data-webform-location-geocomplete-map')) {
$map = $('<div class="webform-location-geocomplete-map"><div class="webform-location-geocomplete-map--container"></div></div>').insertAfter($input).find('.webform-location-geocomplete-map--container');
}
var options = $.extend({
details: $element,
detailsAttribute: 'data-webform-location-geocomplete-attribute',
types: ['geocode'],
map: $map,
geocodeAfterResult: false,
restoreValueAfterBlur: true,
mapOptions: {
disableDefaultUI: true,
zoomControl: true
}
}, Drupal.webform.locationGeocomplete.options);
var $geocomplete = $input.geocomplete(options);
// If there is default value look up location's attributes, else see if
// the default value should be set to the browser's current geolocation.
var value = $geocomplete.val();
if (value) {
$geocomplete.geocomplete('find', value);
}
else if (navigator.geolocation && $geocomplete.attr('data-webform-location-geocomplete-geolocation')) {
navigator.geolocation.getCurrentPosition(function (position) {
$geocomplete.geocomplete('find', position.coords.latitude + ', ' + position.coords.longitude);
});
}
});
}
};
})(jQuery, Drupal);

View file

@ -1,53 +0,0 @@
/**
* @file
* Javascript behaviors for Geocomplete location integration.
*/
(function ($, Drupal, drupalSettings) {
'use strict';
/**
* Initialize location Geocompletion.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformLocation = {
attach: function (context) {
$(context).find('div.js-webform-location').once('webform-location').each(function () {
var $element = $(this);
var $geocomplete = $element.find('.webform-location-geocomplete').geocomplete({
details: $element,
detailsAttribute: 'data-webform-location-attribute',
types: ['geocode']
});
$geocomplete.on('input', function () {
// Reset attributes on input.
$element.find('[data-webform-location-attribute]').val('');
}).on('blur', function () {
// Make sure to get attributes on blur.
if ($element.find('[data-webform-location-attribute="location"]').val() == '') {
var value = $geocomplete.val();
if (value) {
$geocomplete.geocomplete('find', value);
}
}
});
// If there is default value look up location's attributes, else see if
// the default value should be set to the browser's current geolocation.
var value = $geocomplete.val();
if (value) {
$geocomplete.geocomplete('find', value);
}
else if (navigator.geolocation && $geocomplete.attr('data-webform-location-geolocation')) {
navigator.geolocation.getCurrentPosition(function (position) {
$geocomplete.geocomplete('find', position.coords.latitude + ', ' + position.coords.longitude);
});
}
})
}
};
})(jQuery, Drupal, drupalSettings);

View file

@ -0,0 +1,101 @@
/**
* @file
* JavaScript behaviors for Algolia places location integration.
*/
(function ($, Drupal, drupalSettings) {
'use strict';
// @see https://github.com/algolia/places
// @see https://community.algolia.com/places/documentation.html#options
Drupal.webform = Drupal.webform || {};
Drupal.webform.locationPlaces = Drupal.webform.locationPlaces || {};
Drupal.webform.locationPlaces.options = Drupal.webform.locationPlaces.options || {};
var mapping = {
lat: 'lat',
lng: 'lng',
name: 'name',
postcode: 'postcode',
locality: 'locality',
city: 'city',
administrative: 'administrative',
country: 'country',
countryCode: 'country_code',
county: 'county',
suburb: 'suburb'
};
/**
* Initialize location places.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformLocationPlaces = {
attach: function (context) {
if (!window.places) {
return;
}
$(context).find('.js-webform-type-webform-location-places').once('webform-location-places').each(function () {
var $element = $(this);
var $input = $element.find('.webform-location-places');
// Prevent the 'Enter' key from submitting the form.
$input.keydown(function (event) {
if (event.keyCode === 13) {
event.preventDefault();
}
});
var options = $.extend({
type: 'address',
useDeviceLocation: true,
container: $input.get(0)
}, Drupal.webform.locationPlaces.options);
// Add application id and API key.
if (drupalSettings.webform.location.places.app_id && drupalSettings.webform.location.places.api_key) {
options.appId = drupalSettings.webform.location.places.app_id;
options.apiKey = drupalSettings.webform.location.places.api_key;
}
var placesAutocomplete = window.places(options);
// Disable autocomplete.
// @see https://gist.github.com/niksumeiko/360164708c3b326bd1c8
var isChrome = /Chrome/.test(window.navigator.userAgent) && /Google Inc/.test(window.navigator.vendor);
$input.attr('autocomplete', (isChrome) ? 'disabled' : 'false');
// Sync values on change and clear events.
placesAutocomplete.on('change', function (e) {
$.each(mapping, function (source, destination) {
var value = (source === 'lat' || source === 'lng' ? e.suggestion.latlng[source] : e.suggestion[source]) || '';
setValue(destination, value);
});
});
placesAutocomplete.on('clear', function (e) {
$.each(mapping, function (source, destination) {
setValue(destination, '');
});
});
/**
* Set attribute value.
*
* @param {string} name
* The attribute name
* @param {string} value
* The attribute value
*/
function setValue(name, value) {
var inputSelector = ':input[data-webform-location-places-attribute="' + name + '"]';
$element.find(inputSelector).val(value);
}
});
}
};
})(jQuery, Drupal, drupalSettings);

View file

@ -0,0 +1,109 @@
/**
* @file
* JavaScript behaviors for managed file uploads.
*/
(function ($, Drupal) {
'use strict';
/**
* Track file uploads and display confirm dialog when an file upload is inprogress.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformManagedFileAutoUpload = {
attach: function attach(context) {
// Add submit handler to file upload form.
$(context).find('form')
.once('webform-auto-file-upload')
.on('submit', function (event) {
var $form = $(this);
if ($form.data('webform-auto-file-uploads') > 0 && blockSubmit($form)) {
event.preventDefault();
return false;
}
return true;
});
// Add submit handler to form.beforeSend.
// Update Drupal.Ajax.prototype.beforeSend only once.
if (typeof Drupal.Ajax !== 'undefined' && typeof Drupal.Ajax.prototype.beforeSubmitWebformManagedFileAutoUploadOriginal === 'undefined') {
Drupal.Ajax.prototype.beforeSubmitWebformManagedFileAutoUploadOriginal = Drupal.Ajax.prototype.beforeSubmit;
Drupal.Ajax.prototype.beforeSubmit = function (form_values, element_settings, options) {
var $form = this.$form;
var $element = $(this.element);
// Determine if the triggering element is within .form-actions.
var isFormActions = $element
.closest('.form-actions').length;
// Determine if the triggering element is within a multiple element.
var isMultipleUpload = $element
.parents('.js-form-type-webform-multiple, .js-form-type-webform-custom-composite')
.find('.js-form-managed-file').length;
// Determine if the triggering element is not within a
// managed file element.
var isManagedUploadButton = $element.parents('.js-form-managed-file').length;
// Only trigger block submit for .form-actions and multiple element
// with file upload.
if ($form.data('webform-auto-file-uploads') > 0 &&
(isFormActions || (isMultipleUpload && !isManagedUploadButton)) &&
blockSubmit($form)) {
this.ajaxing = false;
return false;
}
return this.beforeSubmitWebformManagedFileAutoUploadOriginal();
};
}
$(context).find('input[type="file"]').once('webform-auto-file-upload').on('change', function () {
// Track file upload.
$(this).data('msk-auto-file-upload', true);
// Increment form file uploads.
var $form = $(this.form);
var fileUploads = ($form.data('webform-auto-file-uploads') || 0);
$form.data('webform-auto-file-uploads', fileUploads + 1);
});
},
detach: function detach(context, settings, trigger) {
if (trigger === 'unload') {
$(context).find('input[type="file"]').removeOnce('webform-auto-file-upload').each(function () {
if ($(this).data('msk-auto-file-upload')) {
// Remove file upload tracking.
$(this).removeData('msk-auto-file-upload');
// Decrease form file uploads.
var $form = $(this.form);
var fileUploads = ($form.data('webform-auto-file-uploads') || 0);
$form.data('webform-auto-file-uploads', fileUploads - 1);
}
});
}
}
};
/**
* Block form submit.
*
* @param {jQuery} form
* A form.
*
* @return {boolean}
* TRUE if form submit should be blocked.
*/
function blockSubmit(form) {
if ($(form).data('webform-auto-file-uploads') < 0) {
return false;
}
var message = Drupal.t('File upload inprogress. Uploaded file may be lost.') +
'\n' +
Drupal.t('Do you want to continue?');
return !window.confirm(message);
}
})(jQuery, Drupal);

View file

@ -1,6 +1,6 @@
/**
* @file
* Javascript behaviors for message element integration.
* JavaScript behaviors for message element integration.
*/
(function ($, Drupal) {
@ -31,13 +31,18 @@
return;
}
$element.show().find('.js-webform-message__link').on('click', function (event) {
// Only show element if it's style is not set to 'display: none'.
if ($element.attr('style') !== 'display: none;') {
$element.show();
}
$element.find('.js-webform-message__link').on('click', function (event) {
$element[effect]();
setClosed($element, storage, id);
$element.trigger('close');
event.preventDefault();
});
})
});
}
};
@ -84,6 +89,7 @@
case 'user':
case 'state':
case 'custom':
$.get($element.find('.js-webform-message__link').attr('href'));
return true;
}

View file

@ -0,0 +1,59 @@
/**
* @file
* JavaScript behaviors for element (read) more.
*/
(function ($, Drupal) {
'use strict';
/**
* Element (read) more.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformElementMore = {
attach: function (context) {
$(context).find('.js-webform-element-more').once('webform-element-more').each(function (event) {
var $more = $(this);
var $a = $more.find('a');
var $content = $more.find('.webform-element-more--content');
// Add aria-* attributes.
$a.attr({
'aria-expanded': false,
'aria-controls': $content.attr('id')
});
// Add event handlers.
$a.on('click', toggle)
.on('keydown', function (event) {
// Space or Return.
if (event.which === 32 || event.which === 13) {
toggle(event);
}
});
function toggle(event) {
var expanded = ($a.attr('aria-expanded') === 'true');
// Toggle `aria-expanded` attributes on link.
$a.attr('aria-expanded', !expanded);
// Toggle content and more .is-open state.
if (expanded) {
$more.removeClass('is-open');
$content.slideUp();
}
else {
$more.addClass('is-open');
$content.slideDown();
}
event.preventDefault();
}
});
}
};
})(jQuery, Drupal);

View file

@ -1,6 +1,6 @@
/**
* @file
* Javascript behaviors for message element integration.
* JavaScript behaviors for multiple element.
*/
(function ($, Drupal) {
@ -12,15 +12,42 @@
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformMultiple = {
Drupal.behaviors.webformMultipleTableDrag = {
attach: function (context, settings) {
for (var base in settings.tableDrag) {
var $tableDrag = $(context).find('#' + base);
var $toggleWeight = $tableDrag.parent().find('.tabledrag-toggle-weight');
$toggleWeight.addClass('webform-multiple-tabledrag-toggle-weight');
$tableDrag.after($toggleWeight);
if (settings.tableDrag.hasOwnProperty(base)) {
$(context).find('.js-form-type-webform-multiple #' + base).once('webform-multiple-table-drag').each(function () {
var $tableDrag = $(this);
var $toggleWeight = $tableDrag.prev().prev('.tabledrag-toggle-weight-wrapper');
if ($toggleWeight.length) {
$toggleWeight.addClass('webform-multiple-tabledrag-toggle-weight');
$tableDrag.after($toggleWeight);
}
});
}
}
}
};
/**
* Submit multiple add number input value when enter is pressed.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformMultipleAdd = {
attach: function (context, settings) {
$(context).find('.js-webform-multiple-add').once('webform-multiple-add').each(function () {
var $submit = $(this).find('input[type="submit"], button');
var $number = $(this).find('input[type="number"]');
$number.keyup(function (event) {
if (event.which === 13) {
// Note: Mousedown is the default trigger for Ajax events.
// @see Drupal.Ajax.
$submit.mousedown();
}
});
});
}
};
})(jQuery, Drupal);

View file

@ -0,0 +1,55 @@
/**
* @file
* JavaScript behaviors for options (admin) elements.
*/
(function ($, Drupal) {
'use strict';
/**
* Attach handlers to options (admin) element.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformOptionsAdmin = {
attach: function (context) {
$(context).find('.js-webform-options-sync').once('js-webform-options-sync').each(function () {
// Target input name and not id because the id will be changing via
// Ajax callbacks.
var name = this.name;
var $value = $(this);
var $text = $('input[name="' + name.replace(/\[value\]$/, '[text]') + '"]');
// On focus, determine if option value and option text are in-sync.
$value.on('focus', function () {
var sync = $value.val() === $text.val();
$value.data('webform_options_sync', sync);
if (sync) {
$text.prop('readonly', true).closest('.js-form-item, .js-form-wrapper').addClass('webform-readonly');
}
});
// On blur, if option value and option text are in-sync remove readonly.
$value.on('blur', function () {
if ($value.data('webform_options_sync')) {
$text.prop('readonly', false).closest('.js-form-item, .js-form-wrapper').removeClass('webform-readonly');
}
});
// On keyup, if option value and option text are in-sync then set
// option text to option value.
$value.on('keyup', function () {
if ($value.data('webform_options_sync')) {
$text.val($value.val());
}
});
});
}
};
})(jQuery, Drupal);

View file

@ -1,11 +1,11 @@
/**
* @file
* Javascript behaviors for other elements.
* JavaScript behaviors for other elements.
*/
(function ($, Drupal) {
"use strict";
'use strict';
/**
* Toggle other input (text) field.
@ -14,21 +14,41 @@
* TRUE will display the text field. FALSE with hide and clear the text field.
* @param {object} $element
* The input (text) field to be toggled.
* @param {string} effect
* Effect.
*/
function toggleOther(show, $element) {
function toggleOther(show, $element, effect) {
var $input = $element.find('input');
var hideEffect = (effect === false) ? 'hide' : 'slideUp';
var showEffect = (effect === false) ? 'show' : 'slideDown';
if (show) {
// Limit the other inputs width to the parent's container.
$element.width($element.parent().width());
// If the parent container is not visible it's width will be 0
// and ignored.
var width = $element.parent().width();
if (width) {
$element.width(width);
}
// Display the element.
$element.slideDown();
// Focus and require the input.
$input.focus().prop('required', true);
$element[showEffect]();
// If not initializing, then focus the other element.
if (effect !== false) {
$input.focus();
}
// Require the input.
$input.prop('required', true).attr('aria-required', 'true');
// Restore the input's value.
var value = $input.data('webform-value');
if (value !== undefined) {
if (typeof value !== 'undefined') {
$input.val(value);
$input.get(0).setSelectionRange(0, 0);
var input = $input.get(0);
// Move cursor to the beginning of the other text input.
// @see https://stackoverflow.com/questions/21177489/selectionstart-selectionend-on-input-type-number-no-longer-allowed-in-chrome
if ($.inArray(input.type, ['text', 'search', 'url', 'tel', 'password']) !== -1) {
input.setSelectionRange(0, 0);
}
}
// Refresh CodeMirror used as other element.
$element.parent().find('.CodeMirror').each(function (index, $element) {
@ -36,11 +56,12 @@
});
}
else {
$element.slideUp();
// Hide the element.
$element[hideEffect]();
// Save the input's value.
$input.data('webform-value', $input.val());
// Empty and un-required the input.
$input.val('').prop('required', false);
$input.val('').prop('required', false).removeAttr('aria-required');
}
}
@ -58,13 +79,11 @@
var $otherOption = $element.find('option[value="_other_"]');
var $input = $element.find('.js-webform-select-other-input');
if ($otherOption.is(':selected')) {
$input.show().find('input').prop('required', true);
}
$select.on('change', function () {
toggleOther($otherOption.is(':selected'), $input);
});
toggleOther($otherOption.is(':selected'), $input, false);
});
}
};
@ -81,13 +100,11 @@
var $checkbox = $element.find('input[value="_other_"]');
var $input = $element.find('.js-webform-checkboxes-other-input');
if ($checkbox.is(':checked')) {
$input.show().find('input').prop('required', true);
}
$checkbox.on('change', function () {
toggleOther(this.checked, $input);
});
toggleOther($checkbox.is(':checked'), $input, false);
});
}
};
@ -105,13 +122,11 @@
var $radios = $element.find('input[type="radio"]');
var $input = $element.find('.js-webform-radios-other-input');
if ($radios.filter(':checked').val() === '_other_') {
$input.show().find('input').prop('required', true);
}
$radios.on('change', function () {
toggleOther(($radios.filter(':checked').val() === '_other_'), $input);
});
toggleOther(($radios.filter(':checked').val() === '_other_'), $input, false);
});
}
};
@ -128,28 +143,14 @@
var $buttons = $element.find('input[type="radio"]');
var $input = $element.find('.js-webform-buttons-other-input');
var $container = $(this).find('.js-webform-webform-buttons');
if ($buttons.filter(':checked').val() === '_other_') {
$input.show().find('input').prop('required', true);
}
// Note: Initializing buttonset here so that we can set the onchange
// event handler.
// @see Drupal.behaviors.webformButtons
var $container = $(this).find('.form-radios');
// Remove all div and classes around radios and labels.
$container.html($container.find('input[type="radio"], label').removeClass());
// Create buttonset and set onchange handler.
$container.buttonset().change(function () {
// Create set onchange handler.
$container.change(function () {
toggleOther(($(this).find(':radio:checked').val() === '_other_'), $input);
});
// Disable buttonset.
$container.buttonset('option', 'disabled', $container.find('input[type="radio"]:disabled').length);
// Turn buttonset off/on when the input is disabled/enabled.
// @see webform.states.js
$container.on('webform:disabled', function () {
$container.buttonset('option', 'disabled', $container.find('input[type="radio"]:disabled').length);
});
toggleOther(($buttons.filter(':checked').val() === '_other_'), $input, false);
});
}
};

View file

@ -1,14 +1,6 @@
/**
* @file
* Javascript behaviors for radio buttons.
*
* Fix #states and #required for radios buttons.
*
* @see Issue #2856795: If radio buttons are required but not filled form is nevertheless submitted.
* @see Issue #2856315: Conditional Logic - Requiring Radios in a Fieldset.
* @see Issue #2731991: Setting required on radios marks all options required.
* @see css/webform.form.css
* @see /core/misc/states.js
* JavaScript behaviors for radio buttons.
*/
(function ($, Drupal) {
@ -16,43 +8,18 @@
'use strict';
/**
* Attach handler to add .js-webform-radios-fieldset to radios wrapper fieldset.
* Adds HTML5 validation to required radios buttons.
*
* @type {Drupal~behavior}
*
* @see Issue #2856795: If radio buttons are required but not filled form is nevertheless submitted.
*/
Drupal.behaviors.webformRadios = {
Drupal.behaviors.webformRadiosRequired = {
attach: function (context) {
$('.js-webform-radios', context).closest('fieldset.form-composite').addClass('js-webform-radios-fieldset');
$('.js-webform-type-radios.required, .js-webform-type-webform-radios-other.required', context).each(function () {
$(this).find('input[type="radio"]').attr({'required': 'required', 'aria-required': 'true'});
});
}
};
// Make absolutely sure the below event handlers are triggered after
// the /core/misc/states.js event handlers by attaching them after DOM load.
$(function () {
Drupal.behaviors.webformRadios.attach($(document));
function setRequired($target, required) {
if (!$target.hasClass('js-webform-radios-fieldset')) {
return;
}
if (required) {
$target.find('input[type="radio"]').attr({'required': 'required', 'aria-required': 'aria-required'})
$target.find('legend span').addClass('js-form-required form-required');
}
else {
$target.find('input[type="radio"]').removeAttr('required aria-required');
$target.find('legend span').removeClass('js-form-required form-required');
}
}
setRequired($('.form-composite[required="required"]'), true);
$(document).on('state:required', function (e) {
if (e.trigger) {
setRequired($(e.target), e.value);
}
});
});
})(jQuery, Drupal);

View file

@ -1,6 +1,6 @@
/**
* @file
* Javascript behaviors for range element integration.
* JavaScript behaviors for range element integration.
*/
(function ($, Drupal) {
@ -8,54 +8,140 @@
'use strict';
/**
* Enhance HTML5 range element.
* Display HTML5 range output in a left/right aligned number input.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformRange = {
Drupal.behaviors.webformRangeOutputNumber = {
attach: function (context) {
$(context).find('.form-range[data-range-output]').once('webform-range').each(function () {
var $element = $(this);
$(context).find('.js-form-type-range').once('webform-range-output-number').each(function () {
// Handle browser that don't support the HTML5 range input.
if (Modernizr.inputtypes.range === false) {
return;
}
var prefix = $element.attr('data-range-output-prefix');
var suffix = $element.attr('data-range-output-suffix');
var $element = $(this);
var $input = $element.find('input[type="range"]');
var $output = $element.find('input[type="number"]');
if (!$output.length) {
return;
}
// Display range input's output to the end user.
var html = '';
html += '<div class="form-range-output-container">';
html += (prefix ? '<span class="field-prefix">' + prefix + '</span>' : '');
html += '<input type="number" min="' + $element.attr('min') + '" max="' + $element.attr('max') + '" step="' + $element.attr('step') + '" class="form-range-output form-number" />';
html += (suffix ? '<span class="field-suffix">' + suffix + '</span>' : '');
html += '</div>';
// Set output value.
$output.val($input.val());
var height = parseInt($element.outerHeight()) || 24;
var $outputContainer = $(html);
// Set the container element's line height which will vertically
// align the range widget and the output.
$outputContainer.find('input, span').css({
height: height + 'px',
lineHeight: height + 'px'
});
var $output = $outputContainer.find('input');
$output[0].value = $element[0].value;
$element
.after($outputContainer)
.css({float: 'left'});
// Sync $element and $output.
$element.on('input', function () {
$output[0].value = $element[0].value;
// Sync input and output values.
$input.on('input', function () {
$output.val($input.val());
});
$output.on('input', function () {
$element[0].value = $output[0].value;
$input.val($output.val());
});
})
});
}
};
/**
* Display HTML5 range output in a floating bubble.
*
* @type {Drupal~behavior}
*
* @see https://css-tricks.com/value-bubbles-for-range-inputs/
* @see https://stackoverflow.com/questions/33794123/absolute-positioning-in-relation-to-a-inputtype-range
*/
Drupal.behaviors.webformRangeOutputBubble = {
attach: function (context) {
$(context).find('.js-form-type-range').once('webform-range-output-bubble').each(function () {
// Handle browser that don't support the HTML5 range input.
if (Modernizr.inputtypes.range === false) {
return;
}
var $element = $(this);
var $input = $element.find('input[type="range"]');
var $output = $element.find('output');
var display = $output.attr('data-display');
if (!$output.length) {
return;
}
$element.css('position', 'relative');
$input.on('input', function () {
var inputValue = $input.val();
// Set output text with prefix and suffix.
var text = ($output.attr('data-field-prefix') || '') +
inputValue +
($output.attr('data-field-suffix') || '');
$output.text(text);
// Set output top position.
var top;
if (display === 'above') {
top = $input.position().top - $output.outerHeight() + 2;
}
else {
top = $input.position().top + $input.outerHeight() + 2;
}
// It is impossible to accurately calculate the exact position of the
// range's buttons so we only incrementally move the output bubble.
var inputWidth = $input.outerWidth();
var buttonPosition = Math.floor(inputWidth * (inputValue - $input.attr('min')) / ($input.attr('max') - $input.attr('min')));
var increment = Math.floor(inputWidth / 5);
var outputWidth = $output.outerWidth();
// Set output left position.
var left;
if (buttonPosition <= increment) {
left = 0;
}
else if (buttonPosition <= increment * 2) {
left = (increment * 1.5) - outputWidth;
if (left < 0) {
left = 0;
}
}
else if (buttonPosition <= increment * 3) {
left = (increment * 2.5) - (outputWidth / 2);
}
else if (buttonPosition <= increment * 4) {
left = (increment * 4) - outputWidth;
if (left > (increment * 5) - outputWidth) {
left = (increment * 5) - outputWidth;
}
}
else if (buttonPosition <= inputWidth) {
left = (increment * 5) - outputWidth;
}
// Also make sure to include the input's left position.
left = Math.floor($input.position().left + left);
// Finally, position the output.
$output.css({top: top, left: left});
})
// Fake a change to position output at page load.
.trigger('input');
// Add fade in/out event handlers if opacity is defined.
var defaultOpacity = $output.css('opacity');
if (defaultOpacity < 1) {
// Fade in/out on focus/blur of the input.
$input.on('focus mouseover', function () {
$output.stop().fadeTo('slow', 1);
});
$input.on('blur mouseout', function () {
$output.stop().fadeTo('slow', defaultOpacity);
});
// Also fade in when focusing the output.
$output.on('touchstart mouseover', function () {
$output.stop().fadeTo('slow', 1);
});
}
});
}
};

View file

@ -1,12 +1,15 @@
/**
* @file
* Javascript behaviors for RateIt integration.
* JavaScript behaviors for RateIt integration.
*/
(function ($, Drupal) {
'use strict';
// All options can be override using custom data-* attributes.
// @see https://github.com/gjunge/rateit.js/wiki#options.
/**
* Initialize rating element using RateIt.
*
@ -14,6 +17,10 @@
*/
Drupal.behaviors.webformRating = {
attach: function (context) {
if (!$.fn.rateit) {
return;
}
$(context)
.find('[data-rateit-backingfld]')
.once('webform-rating')
@ -21,6 +28,11 @@
var $rateit = $(this);
var $input = $($rateit.attr('data-rateit-backingfld'));
// Rateit only initialize inputs on load.
if (document.readyState === 'complete') {
$rateit.rateit();
}
// Update the RateIt widget when the input's value has changed.
// @see webform.states.js
$input.on('change', function () {

View file

@ -1,6 +1,6 @@
/**
* @file
* Javascript behaviors for roles element integration.
* JavaScript behaviors for roles element integration.
*/
(function ($, Drupal) {
@ -17,7 +17,7 @@
$(context).find('.js-webform-roles-role[value="authenticated"]').once('webform-roles').each(function () {
var $authenticated = $(this);
var $checkboxes = $authenticated.parents('.form-checkboxes').find('.js-webform-roles-role').filter(function () {
return ($(this).val() != 'anonymous' && $(this).val() != 'authenticated');
return ($(this).val() !== 'anonymous' && $(this).val() !== 'authenticated');
});
$authenticated.on('click', function () {
@ -32,7 +32,7 @@
if ($authenticated.is(':checked')) {
$checkboxes.prop('checked', true).attr('disabled', true);
}
})
});
}
};

View file

@ -1,12 +1,17 @@
/**
* @file
* Javascript behaviors for Select2 integration.
* JavaScript behaviors for Select2 integration.
*/
(function ($, Drupal) {
'use strict';
// @see https://select2.github.io/options.html
Drupal.webform = Drupal.webform || {};
Drupal.webform.select2 = Drupal.webform.select2 || {};
Drupal.webform.select2.options = Drupal.webform.select2.options || {};
/**
* Initialize Select2 support.
*
@ -14,13 +19,55 @@
*/
Drupal.behaviors.webformSelect2 = {
attach: function (context) {
if (!$.fn.select2) {
return;
}
$(context)
.find('select.js-webform-select2, .js-webform-select2 select')
.once('webform-select2')
// http://stackoverflow.com/questions/14313001/select2-not-calculating-resolved-width-correctly-if-select-is-hidden
.css('width', '100%')
.select2();
.each(function () {
var $select = $(this);
var options = $.extend({}, Drupal.webform.select2.options);
if ($select.data('placeholder')) {
options.placeholder = $select.data('placeholder');
if (!$select.prop('multiple')) {
// Allow single option to be deselected.
options.allowClear = true;
}
}
$select.select2(options);
});
}
};
/**
* ISSUE:
* Hiding/showing element via #states API cause select2 dropdown to appear in the wrong position.
*
* WORKAROUND:
* Close (aka hide) select2 dropdown when #states API hides or shows an element.
*
* Steps to reproduce:
* - Add custom 'Submit button(s)'
* - Hide submit button
* - Save
* - Open 'Submit button(s)' dialog
*
* Dropdown body is positioned incorrectly when dropdownParent isn't statically positioned.
* @see https://github.com/select2/select2/issues/3303
*/
$(function () {
if ($.fn.select2) {
$(document).on('state:visible state:visible-slide', function (e) {
$('select.select2-hidden-accessible').select2('close');
});
}
});
})(jQuery, Drupal);

View file

@ -1,12 +1,17 @@
/**
* @file
* Javascript behaviors for signature pad integration.
* JavaScript behaviors for signature pad integration.
*/
(function ($, Drupal) {
'use strict';
// @see https://github.com/szimek/signature_pad#options
Drupal.webform = Drupal.webform || {};
Drupal.webform.signaturePad = Drupal.webform.signaturePad || {};
Drupal.webform.signaturePad.options = Drupal.webform.signaturePad.options || {};
/**
* Initialize signature element.
*
@ -14,20 +19,29 @@
*/
Drupal.behaviors.webformSignature = {
attach: function (context) {
if (!window.SignaturePad) {
return;
}
$(context).find('input.js-webform-signature').once('webform-signature').each(function () {
var $input = $(this);
var value = $input.val();
var $wrapper = $input.parent();
var $canvas = $wrapper.find('canvas');
var $button = $wrapper.find('input[type="submit"]');
var $button = $wrapper.find(':button, :submit');
var canvas = $canvas[0];
var calculateDimensions = function () {
$canvas.attr('width', $wrapper.width());
$canvas.attr('height', $wrapper.width() / 3);
};
// Set height.
$canvas.attr('width', $wrapper.width());
$canvas.attr('height', $wrapper.width() / 3);
$(window).resize(function () {
$canvas.attr('width', $wrapper.width());
$canvas.attr('height', $wrapper.width() / 3);
calculateDimensions();
// Resizing clears the canvas so we need to reset the signature pad.
signaturePad.clear();
@ -38,11 +52,12 @@
});
// Initialize signature canvas.
var signaturePad = new SignaturePad(canvas, {
'onEnd': function () {
var options = $.extend({
onEnd: function () {
$input.val(signaturePad.toDataURL());
}
});
}, Drupal.webform.signaturePad.options);
var signaturePad = new SignaturePad(canvas, options);
// Set value.
if (value) {
@ -52,17 +67,23 @@
// Set reset handler.
$button.on('click', function () {
signaturePad.clear();
$input.val();
$input.val('');
this.blur();
return false;
});
// Input onchange clears signature pad if value is empty.
// Onchange events handlers are triggered when a webform is
// hidden or shown.
// @see webform.states.js
// @see triggerEventHandlers()
$input.on('change', function () {
if (!$input.val()) {
signaturePad.clear();
}
setTimeout(function () {
calculateDimensions();
}, 1);
});
// Turn signature pad off/on when the input is disabled/enabled.

View file

@ -0,0 +1,66 @@
/**
* @file
* JavaScript behaviors for tableselect enhancements.
*
* @see core/misc/tableselect.es6.js
*/
(function ($, Drupal) {
'use strict';
/**
* Initialize and tweak webform tableselect behavior.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformTableSelect = {
attach: function (context) {
$(context)
.find('table.js-webform-tableselect')
.once('webform-tableselect')
.each(Drupal.webformTableSelect);
}
};
/**
* Callback used in {@link Drupal.behaviors.tableSelect}.
*/
Drupal.webformTableSelect = function () {
var $table = $(this);
// Set default table rows to .selected class.
$table.find('tr').each(function () {
// Set table row selected for checkboxes.
var $tr = $(this);
if ($tr.find('input[type="checkbox"]:checked').length && !$tr.hasClass('selected')) {
$tr.addClass('selected');
}
});
// Add .selected class event handler to all tableselect elements.
// Currently .selected is only added to tables with .select-all.
if ($table.find('th.select-all').length === 0) {
$table.find('td input[type="checkbox"]:enabled').on('click', function () {
$(this).closest('tr').toggleClass('selected', this.checked);
});
}
// Add click event handler to the table row that toggles the
// checkbox or radio.
$table.find('tr').on('click', function (event) {
if ($.inArray(event.target.tagName, ['A', 'BUTTON', 'INPUT', 'SELECT']) !== -1) {
return true;
}
var $tr = $(this);
var $checkbox = $tr.find('td input[type="checkbox"]:enabled, td input[type="radio"]:enabled');
if ($checkbox.length === 0) {
return true;
}
$checkbox.click();
});
};
})(jQuery, Drupal);

View file

@ -1,12 +1,17 @@
/**
* @file
* Javascript behaviors for Telephone element.
* JavaScript behaviors for Telephone element.
*/
(function ($, Drupal, drupalSettings) {
'use strict';
// @see https://github.com/jackocnr/intl-tel-input#options
Drupal.webform = Drupal.webform || {};
Drupal.webform.intlTelInput = Drupal.webform.intlTelInput || {};
Drupal.webform.intlTelInput.options = Drupal.webform.intlTelInput.options || {};
/**
* Initialize Telephone international element.
* @see http://intl-tel-input.com/node_modules/intl-tel-input/examples/gen/is-valid-number.html
@ -14,25 +19,31 @@
*/
Drupal.behaviors.webformTelephoneInternational = {
attach: function (context) {
if (!$.fn.intlTelInput) {
return;
}
$(context).find('input.js-webform-telephone-international').once('webform-telephone-international').each(function () {
var $telephone = $(this);
// Add error message container.
var $error = $('<div class="form-item--error-message">' + Drupal.t('Invalid phone number') + '</div>').hide();
$telephone.closest('.form-item').append($error);
$telephone.closest('.js-form-item').append($error);
// @todo: Figure out how to lazy load utilsScript (build/js/utils.js).
// @see https://github.com/jackocnr/intl-tel-input#utilities-script
$telephone.intlTelInput({
'nationalMode': false
});
var options = $.extend({
nationalMode: false,
initialCountry: $telephone.attr('data-webform-telephone-international-initial-country') || ''
}, Drupal.webform.intlTelInput.options);
$telephone.intlTelInput(options);
var reset = function() {
var reset = function () {
$telephone.removeClass('error');
$error.hide();
};
$telephone.blur(function() {
$telephone.blur(function () {
reset();
if ($.trim($telephone.val())) {
if (!$telephone.intlTelInput('isValidNumber')) {
@ -43,7 +54,7 @@
});
$telephone.on('keyup change', reset);
})
});
}
};

View file

@ -0,0 +1,83 @@
/**
* @file
* JavaScript behaviors for terms of service.
*/
(function ($, Drupal) {
'use strict';
// @see http://api.jqueryui.com/dialog/
Drupal.webform = Drupal.webform || {};
Drupal.webform.termsOfServiceModal = Drupal.webform.termsOfServiceModal || {};
Drupal.webform.termsOfServiceModal.options = Drupal.webform.termsOfServiceModal.options || {};
/**
* Initialize terms of service element.
*
* @type {Drupal~behavior}
*/
Drupal.behaviors.webformTermsOfService = {
attach: function (context) {
$(context).find('.js-form-type-webform-terms-of-service').once('webform-terms-of-service').each(function () {
var $element = $(this);
var $a = $element.find('label a');
var $details = $element.find('.webform-terms-of-service-details');
var type = $element.attr('data-webform-terms-of-service-type');
// Initialize the modal.
if (type === 'modal') {
// Move details title to attribute.
var $title = $element.find('.webform-terms-of-service-details--title');
if ($title.length) {
$details.attr('title', $title.text());
$title.remove();
}
var options = $.extend({
modal: true,
autoOpen: false,
minWidth: 600,
maxWidth: 800
}, Drupal.webform.termsOfServiceModal.options);
$details.dialog(options);
}
// Add aria-* attributes.
if (type !== 'modal') {
$a.attr({
'aria-expanded': false,
'aria-controls': $details.attr('id')
});
}
// Set event handlers.
$a.click(openDetails)
.on('keydown', function (event) {
// Space or Return.
if (event.which === 32 || event.which === 13) {
openDetails(event);
}
});
function openDetails(event) {
if (type === 'modal') {
$details.dialog('open');
}
else {
var expanded = ($a.attr('aria-expanded') === 'true');
// Toggle `aria-expanded` attributes on link.
$a.attr('aria-expanded', !expanded);
// Toggle details.
$details[expanded ? 'slideUp' : 'slideDown']();
}
event.preventDefault();
}
});
}
};
})(jQuery, Drupal);

View file

@ -1,6 +1,6 @@
/**
* @file
* Javascript behaviors for Text format integration.
* JavaScript behaviors for Text format integration.
*/
(function ($, Drupal) {
@ -15,11 +15,11 @@
Drupal.behaviors.webformTextFormat = {
attach: function (context) {
$(context).find('.js-text-format-wrapper textarea').once('webform-text-format').each(function () {
var $textarea = $(this);
if (!window.CKEDITOR) {
return;
}
var $textarea = $(this);
// Update the CKEDITOR when the textarea's value has changed.
// @see webform.states.js
$textarea.on('change', function () {

View file

@ -1,12 +1,17 @@
/**
* @file
* Javascript behaviors for time integration.
* JavaScript behaviors for time integration.
*/
(function ($, Drupal) {
'use strict';
// @see https://github.com/jonthornton/jquery-timepicker#options
Drupal.webform = Drupal.webform || {};
Drupal.webform.timePicker = Drupal.webform.timePicker || {};
Drupal.webform.timePicker.options = Drupal.webform.timePicker.options || {};
/**
* Attach timepicker fallback on time elements.
*
@ -17,18 +22,21 @@
*/
Drupal.behaviors.webformTime = {
attach: function (context, settings) {
var $context = $(context);
// Skip if time inputs are supported by the browser.
if (Modernizr.inputtypes.time === true) {
if (!$.fn.timepicker) {
return;
}
$context.find('input[type="time"]').once('timePicker').each(function () {
$(context).find('input[data-webform-time-format]').once('webformTimePicker').each(function () {
var $input = $(this);
var options = {};
if ($input.data('webformTimeFormat')) {
options.timeFormat = $input.data('webformTimeFormat');
// Skip if time inputs are supported by the browser and input is not a text field.
// @see \Drupal\webform\Element\WebformDatetime
if (window.Modernizr && Modernizr.inputtypes.time === true && $input.attr('type') !== 'text') {
return;
}
var options = {};
options.timeFormat = $input.data('webformTimeFormat');
if ($input.attr('min')) {
options.minTime = $input.attr('min');
}
@ -49,9 +57,11 @@
options.step = 1;
}
options = $.extend(options, Drupal.webform.timePicker.options);
$input.timepicker(options);
});
}
}
};
})(jQuery, Drupal);

View file

@ -1,12 +1,17 @@
/**
* @file
* Javascript behaviors for toggle integration.
* JavaScript behaviors for toggle integration.
*/
(function ($, Drupal) {
'use strict';
// @see https://github.com/simontabor/jquery-toggles
Drupal.webform = Drupal.webform || {};
Drupal.webform.toggles = Drupal.webform.toggles || {};
Drupal.webform.toggles.options = Drupal.webform.toggles.options || {};
/**
* Initialize toggle element using Toggles.
*
@ -14,13 +19,17 @@
*/
Drupal.behaviors.webformToggle = {
attach: function (context) {
if (!$.fn.toggles) {
return;
}
$(context).find('.js-webform-toggle').once('webform-toggle').each(function () {
var $toggle = $(this);
var $wrapper = $toggle.parent();
var $checkbox = $wrapper.find('input[type="checkbox"]');
var $label = $wrapper.find('label');
$toggle.toggles({
var options = $.extend({
checkbox: $checkbox,
on: $checkbox.is(':checked'),
clicker: $label,
@ -28,6 +37,14 @@
on: $toggle.attr('data-toggle-text-on') || '',
off: $toggle.attr('data-toggle-text-off') || ''
}
}, Drupal.webform.toggles.options);
$toggle.toggles(options);
// Trigger change event for #states API.
// @see Drupal.states.Trigger.states.checked.change
$toggle.on('toggle', function () {
$checkbox.trigger('change');
});
// If checkbox is disabled then add the .disabled class to the toggle.
@ -36,21 +53,22 @@
}
// Add .clearfix to the wrapper.
$wrapper.addClass('clearfix')
$wrapper.addClass('clearfix');
});
}
};
// Track the disabling of a toggle's checkbox using states.
$(document).on('state:disabled', function (event) {
$('.js-webform-toggle').each(function () {
var $toggle = $(this);
var $wrapper = $toggle.parent();
var $checkbox = $wrapper.find('input[type="checkbox"]');
var isDisabled = ($checkbox.attr('disabled') || $checkbox.attr('readonly'));
(isDisabled) ? $toggle.addClass('disabled') : $toggle.removeClass('disabled');
if ($.fn.toggles) {
$(document).on('state:disabled', function (event) {
$('.js-webform-toggle').each(function () {
var $toggle = $(this);
var $wrapper = $toggle.parent();
var $checkbox = $wrapper.find('input[type="checkbox"]');
var isDisabled = ($checkbox.attr('disabled') || $checkbox.attr('readonly'));
$toggle[isDisabled ? 'disabled' : 'disabled']();
});
});
});
}
})(jQuery, Drupal);

View file

@ -1,10 +1,16 @@
/**
* @file
* Javascript to disable back button.
* JavaScript to disable back button.
*/
// 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";
};
(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

@ -1,12 +1,34 @@
/**
* @file
* Javascript behaviors for webforms.
* 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.weformRemoveFormSingleSubmit = {
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:not([method~="GET"])', onFormSubmit);
}
};
/**
* Autofocus first input.
*
@ -28,17 +50,21 @@
*
* @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) {
// @see http://stackoverflow.com/questions/11235622/jquery-disable-form-submit-on-enter
$(context).find('.webform-submission-form.js-webform-disable-autosubmit input').once('webform-disable-autosubmit').on('keyup keypress', function (e) {
var keyCode = e.keyCode || e.which;
if (keyCode === 13) {
e.preventDefault();
return false;
}
});
$(context).find('.webform-submission-form.js-webform-disable-autosubmit input')
.not(':button, :submit, :reset, :image, :file')
.once('webform-disable-autosubmit')
.on('keyup keypress', function (e) {
var keyCode = e.keyCode || e.which;
if (keyCode === 13) {
e.preventDefault();
return false;
}
});
}
};
@ -52,90 +78,71 @@
*/
Drupal.behaviors.webformSubmitNoValidate = {
attach: function (context) {
$(context).find('input:submit.js-webform-novalidate').once('webform-novalidate').on('click', function () {
$(context).find(':submit.js-webform-novalidate').once('webform-novalidate').on('click', function () {
$(this.form).attr('novalidate', 'novalidate');
});
}
};
/**
* Disable validate when save draft submit button is clicked.
* Attach behaviors to trigger submit button from input onchange.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the behavior for the webform draft submit button.
* Attaches form trigger submit events.
*/
Drupal.behaviors.webformDraft = {
Drupal.behaviors.webformSubmitTrigger = {
attach: function (context) {
$(context).find('#edit-draft').once('webform-draft').on('click', function () {
$(this.form).attr('novalidate', 'novalidate');
$('[data-webform-trigger-submit]').once('webform-trigger-submit').on('change', function () {
var submit = $(this).attr('data-webform-trigger-submit');
$(submit).mousedown();
});
}
};
/**
* Filters the webform element list by a text input search string.
*
* The text input will have the selector `input.webform-form-filter-text`.
*
* The target element to do searching in will be in the selector
* `input.webform-form-filter-text[data-element]`
*
* The text source where the text should be found will have the selector
* `.webform-form-filter-text-source`
* Custom required error message.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the behavior for the webform element filtering.
* Attaches the behavior for the webform custom required error message.
*
* @see http://stackoverflow.com/questions/5272433/html5-form-required-attribute-set-custom-validation-message
*/
Drupal.behaviors.webformFilterByText = {
attach: function (context, settings) {
var $input = $('input.webform-form-filter-text').once('webform-form-filter-text');
var $table = $($input.attr('data-element'));
var $filter_rows;
/**
* Filters the webform element list.
*
* @param {jQuery.Event} e
* The jQuery event for the keyup event that triggered the filter.
*/
function filterElementList(e) {
var query = $(e.target).val().toLowerCase();
/**
* Shows or hides the webform element entry based on the query.
*
* @param {number} index
* The index in the loop, as provided by `jQuery.each`
* @param {HTMLElement} label
* The label of the webform.
*/
function toggleEntry(index, label) {
var $label = $(label);
var $row = $label.parent().parent();
var textMatch = $label.text().toLowerCase().indexOf(query) !== -1;
$row.toggle(textMatch);
}
// Filter if the length of the query is at least 2 characters.
if (query.length >= 2) {
$filter_rows.each(toggleEntry);
}
else {
$filter_rows.each(function (index) {
$(this).parent().parent().show();
Drupal.behaviors.webformRequiredError = {
attach: function (context) {
$(context).find(':input[data-webform-required-error]').once('webform-required-error')
.on('invalid', function () {
this.setCustomValidity('');
if (!this.valid) {
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('');
});
}
}
if ($table.length) {
$filter_rows = $table.find('div.webform-form-filter-text-source');
$input.on('keyup', filterElementList);
}
});
}
};
// 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('');});
});
if (window.imceInput) {
window.imceInput.processUrlInput = function (i, el) {
var button = imceInput.createUrlButton(el.id, el.getAttribute('data-imce-type'));
el.parentNode.insertAfter(button, el);
};
}
})(jQuery, Drupal);

View file

@ -0,0 +1,18 @@
/**
* @file
* JavaScript to allow back button submit wizard page.
*/
(function ($) {
'use strict';
// From: https://stackoverflow.com/a/39019647
if (window.history && window.history.pushState) {
window.history.pushState('', null, '');
window.onpopstate = function (event) {
$('#edit-wizard-prev, #edit-preview-prev').click();
};
}
})(jQuery);

View file

@ -1,6 +1,6 @@
/**
* @file
* Javascript behaviors for preventing duplicate webform submissions.
* JavaScript behaviors for preventing duplicate webform submissions.
*/
(function ($, Drupal) {
@ -19,34 +19,43 @@
attach: function (context) {
$('.js-webform-submit-once', context).each(function () {
var $form = $(this);
$form.removeAttr('webform-submitted');
$form.find('#edit-actions input[type="submit"]').removeAttr('webform-clicked');
// Remove data-webform-submitted.
$form.removeData('webform-submitted');
// Remove .js-webform-submit-clicked.
$form.find('.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('#edit-actions input[type="submit"]').click(function () {
$form.find('#edit-actions input[type="submit"]').removeAttr('webform-clicked');
$(this).attr('webform-clicked', 'true');
$form.find('.form-actions :submit').click(function () {
$form.find('.form-actions :submit')
.removeClass('js-webform-submit-clicked');
$(this)
.addClass('js-webform-submit-clicked');
});
$(this).submit(function () {
// Track webform submitted.
if ($form.attr('webform-submitted')) {
// Don't submit if client-side validation has failed.
if ($.isFunction(jQuery.fn.valid) && !($form.valid())) {
return false;
}
$form.attr('webform-submitted', 'true');
// 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('#edit-actions input[type="submit"]').addClass('is-disabled');
$form.find('.form-actions :submit').addClass('is-disabled');
// Set the throbber progress indicator.
// @see Drupal.Ajax.prototype.setProgressIndicatorThrobber
var $clickedButton = $form.find('#edit-actions input[type=submit][webform-clicked=true]');
var $clickedButton = $form.find('.form-actions :submit.js-webform-submit-clicked');
var $progress = $('<div class="ajax-progress ajax-progress-throbber"><div class="throbber">&nbsp;</div></div>');
$clickedButton.after($progress);
});
})
});
}
};

View file

@ -0,0 +1,48 @@
/**
* @file
* JavaScript behaviors for form tabs using jQuery UI.
*/
(function ($, Drupal) {
'use strict';
// @see http://api.jqueryui.com/tabs/
Drupal.webform = Drupal.webform || {};
Drupal.webform.formTabs = Drupal.webform.formTabs || {};
Drupal.webform.formTabs.options = Drupal.webform.formTabs.options || {
hide: true,
show: true
};
/**
* Initialize webform tabs.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the behavior for form tabs using jQuery UI.
*
* @see \Drupal\webform\Utility\WebformFormHelper::buildTabs
*/
Drupal.behaviors.webformFormTabs = {
attach: function (context) {
$(context).find('div.webform-tabs').once('webform-tabs').each(function () {
var $tabs = $(this);
var options = jQuery.extend({}, Drupal.webform.formTabs.options);
// Set active tab and clear the location hash once it is set.
if (location.hash) {
var active = $('a[href="' + location.hash + '"]').data('tab-index');
if (typeof active !== 'undefined') {
options.active = active;
location.hash = '';
}
}
$tabs.tabs(options);
});
}
};
})(jQuery, Drupal);

View file

@ -1,6 +1,6 @@
/**
* @file
* Javascript behaviors for unsaved webforms.
* JavaScript behaviors for unsaved webforms.
*/
(function ($, Drupal) {
@ -26,14 +26,38 @@
unsaved = true;
}
else {
$('.js-webform-unsaved :input:not(input[type=\'submit\'])', context).once('webform-unsaved').on('change keypress', function () {
unsaved = true;
$('.js-webform-unsaved :input:not(:button, :submit, :reset)', context).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').on('click', function () {
$('.js-webform-unsaved button, .js-webform-unsaved input[type="submit"]', context).once('webform-unsaved').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;
});
// 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;
});
});
}
}
};
@ -43,7 +67,7 @@
}
});
/*!
/**
* An experimental shim to partially emulate onBeforeUnload on iOS.
* Part of https://github.com/codedance/jquery.AreYouSure/
*
@ -60,9 +84,9 @@
}
$('a').bind('click', function (evt) {
var href = $(evt.target).closest('a').attr('href');
if (href !== undefined && !(href.match(/^#/) || href.trim() == '')) {
if (typeof href !== 'undefined' && !(href.match(/^#/) || href.trim() === '')) {
if ($(window).triggerHandler('beforeunload')) {
if (!confirm(Drupal.t('Changes you made may not be saved.') + '\n\n' + Drupal.t('Press OK to leave this page or Cancel to stay.'))) {
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;
}
}

View file

@ -1,36 +1,12 @@
/**
* @file
* Javascript behaviors for help.
* JavaScript behaviors for help.
*/
(function ($, Drupal) {
'use strict';
/**
* Handles help accordion.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches the behavior for help accordion.
*/
Drupal.behaviors.webformHelpAccordion = {
attach: function (context) {
var $widget = $(context).find('.webform-help-accordion');
$widget.once('webform-help-accordion').accordion({
collapsible: true,
heightStyle: "content"
});
var $container = $('h3' + location.hash, $widget);
if ($container.length) {
var active = $widget.find($widget.accordion('option', 'header')).index($container);
$widget.accordion('option', 'active', active);
}
}
};
/**
* Handles disabling help dialog for mobile devices.
*
@ -47,7 +23,7 @@
}
}).each(function () {
// Must make sure that this click event handler is execute first and
// before the AJAX dialog handler.
// before the Ajax dialog handler.
// @see http://stackoverflow.com/questions/2360655/jquery-event-handlers-always-execute-in-order-they-were-bound-any-way-around-t
var handlers = $._data(this, 'events')['click'];
var handler = handlers.pop();

View file

@ -0,0 +1,23 @@
/**
* @file
* JavaScript behaviors for IMCE.
*/
(function ($, Drupal) {
'use strict';
/**
* Override processUrlInput to place the 'Open File Browser' links after the target element.
*
* @param {int} i
* Element's index.
* @param {element} el
* A element.
*/
window.imceInput.processUrlInput = function (i, el) {
var button = imceInput.createUrlButton(el.id, el.getAttribute('data-imce-type'));
$(button).insertAfter($(el));
};
})(jQuery, Drupal);

View file

@ -0,0 +1,52 @@
/**
* @file
* JavaScript behaviors to fix jQuery UI dialogs.
*/
(function ($, Drupal) {
'use strict';
/**
* Ensure that ckeditor has focus when displayed inside of jquery-ui dialog widget
*
* @see http://stackoverflow.com/questions/20533487/how-to-ensure-that-ckeditor-has-focus-when-displayed-inside-of-jquery-ui-dialog
*/
var _allowInteraction = $.ui.dialog.prototype._allowInteraction;
$.ui.dialog.prototype._allowInteraction = function (event) {
if ($(event.target).closest('.cke_dialog').length) {
return true;
}
return _allowInteraction.apply(this, arguments);
};
/**
* Attaches webform dialog behaviors.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches event listeners for webform dialogs.
*/
Drupal.behaviors.webformDialogEvents = {
attach: function () {
$(window).once('webform-dialog').on({
'dialog:aftercreate': function (event, dialog, $element, settings) {
setTimeout(function () {
var hasFocus = $element.find('[autofocus]:tabbable');
if (!hasFocus.length) {
// Move focus to first input which is not a button.
hasFocus = $element.find(':input:tabbable:not(:button)');
}
if (!hasFocus.length) {
// Move focus to close dialog button.
hasFocus = $element.parent().find('.ui-dialog-titlebar-close');
}
hasFocus.eq(0).focus();
});
}
});
}
};
})(jQuery, Drupal);

View file

@ -0,0 +1,39 @@
/**
* @file
* JavaScript behaviors for webform off-canvas dialogs.
*
* @see misc/dialog/off-canvas.js
*/
(function ($, Drupal) {
'use strict';
/**
* Attaches webform off-canvas behaviors.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Attaches event listeners to window for off-canvas dialogs.
*/
Drupal.behaviors.webformOffCanvasEvents = {
attach: function () {
// Resize seven.theme tabs when off-canvas dialog opened and closed.
// @see core/themes/seven/js/nav-tabs.js
$(window).once('webform-off-canvas').on({
'dialog:aftercreate': function (event, dialog, $element, settings) {
if (Drupal.offCanvas.isOffCanvas($element)) {
$(window).trigger('resize.tabs');
}
},
'dialog:afterclose': function (event, dialog, $element, settings) {
if (Drupal.offCanvas.isOffCanvas($element)) {
$(window).trigger('resize.tabs');
}
}
});
}
};
})(jQuery, Drupal);

View file

@ -1,80 +1,277 @@
/**
* @file
* Javascript behaviors for custom webform #states.
* JavaScript behaviors for custom webform #states.
*/
(function ($, Drupal) {
'use strict';
// Make absolutely sure the below event handlers are triggered after
// the /core/misc/states.js event handlers by attaching them after DOM load.
$(function () {
var $document = $(document);
$document.on('state:visible', function (e) {
if (!e.trigger) {
return TRUE;
Drupal.webform = Drupal.webform || {};
Drupal.webform.states = Drupal.webform.states || {};
Drupal.webform.states.slideDown = Drupal.webform.states.slideDown || {};
Drupal.webform.states.slideDown.duration = 'slow';
Drupal.webform.states.slideUp = Drupal.webform.states.slideUp || {};
Drupal.webform.states.slideUp.duration = 'fast';
/**
* Check if an element has a specified data attribute.
*
* @param {string} data
* The data attribute name.
*
* @return {boolean}
* TRUE if an element has a specified data attribute.
*/
$.fn.hasData = function (data) {
return (typeof this.data(data) !== 'undefined');
};
/**
* Check if element is within the webform or not.
*
* @return {boolean}
* TRUE if element is within the webform.
*/
$.fn.isWebform = function () {
return $(this).closest('form[id^="webform"]').length ? true : false;
};
// The change event is triggered by cut-n-paste and select menus.
// Issue #2445271: #states element empty check not triggered on mouse
// based paste.
// @see https://www.drupal.org/node/2445271
Drupal.states.Trigger.states.empty.change = function change() {
return this.val() === '';
};
// Apply solution included in #1962800 patch.
// Issue #1962800: Form #states not working with literal integers as
// values in IE11.
// @see https://www.drupal.org/project/drupal/issues/1962800
// @see https://www.drupal.org/files/issues/core-states-not-working-with-integers-ie11_1962800_46.patch
//
// This issue causes pattern, less than, and greater than support to break.
// @see https://www.drupal.org/project/webform/issues/2981724
var states = Drupal.states;
Drupal.states.Dependent.prototype.compare = function compare(reference, selector, state) {
var value = this.values[selector][state.name];
var name = reference.constructor.name;
if (!name) {
name = $.type(reference);
name = name.charAt(0).toUpperCase() + name.slice(1);
}
if (name in states.Dependent.comparisons) {
return states.Dependent.comparisons[name](reference, value);
}
if (reference.constructor.name in states.Dependent.comparisons) {
return states.Dependent.comparisons[reference.constructor.name](reference, value);
}
return _compare2(reference, value);
};
function _compare2(a, b) {
if (a === b) {
return typeof a === 'undefined' ? a : true;
}
return typeof a === 'undefined' || typeof b === 'undefined';
}
// Adds pattern, less than, and greater than support to #state API.
// @see http://drupalsun.com/julia-evans/2012/03/09/extending-form-api-states-regular-expressions
Drupal.states.Dependent.comparisons.Object = function (reference, value) {
if ('pattern' in reference) {
return (new RegExp(reference['pattern'])).test(value);
}
else if ('!pattern' in reference) {
return !((new RegExp(reference['!pattern'])).test(value));
}
else if ('less' in reference) {
return (value !== '' && parseFloat(reference['less']) > parseFloat(value));
}
else if ('greater' in reference) {
return (value !== '' && parseFloat(reference['greater']) < parseFloat(value));
}
else {
return reference.indexOf(value) !== false;
}
};
var $document = $(document);
$document.on('state:required', function (e) {
if (e.trigger && $(e.target).isWebform()) {
var $target = $(e.target);
// Fix #required file upload.
// @see Issue #2860529: Conditional required File upload field don't work.
if (e.value) {
$target.find('input[type="file"]').attr({'required': 'required', 'aria-required': 'true'});
}
else {
$target.find('input[type="file"]').removeAttr('required aria-required');
}
if (!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')) {
if (e.value) {
$target.find('legend span:not(.visually-hidden)').addClass('js-form-required form-required');
}
else {
$target.find('legend span:not(.visually-hidden)').removeClass('js-form-required form-required');
}
}
// Fix #required for radios.
// @see Issue #2856795: If radio buttons are required but not filled form is nevertheless submitted.
if ($target.is('.js-webform-type-radios, .js-form-type-webform-radios-other')) {
if (e.value) {
$target.find('input[type="radio"]').attr({'required': 'required', 'aria-required': 'true'});
}
else {
$target.find('input[type="radio"]').removeAttr('required aria-required');
}
}
// Fix #required for checkboxes.
// @see Issue #2938414: Checkboxes don't support #states required.
// @see checkboxRequiredhandler
if ($target.is('.js-webform-type-checkboxes, .js-form-type-webform-checkboxes-other')) {
var $checkboxes = $target.find('input[type="checkbox"]');
if (e.value) {
// Bind the event handler and add custom HTML5 required validation
// to all checkboxes.
$checkboxes.bind('click', checkboxRequiredhandler);
if (!$checkboxes.is(':checked')) {
$checkboxes.attr({'required': 'required', 'aria-required': 'true'});
}
}
else {
// Remove custom HTML5 required validation from all checkboxes
// and unbind the event handler.
$checkboxes
.removeAttr('required aria-required')
.unbind('click', checkboxRequiredhandler);
}
}
// Issue #2986017: Fieldsets shouldn't have required attribute.
if ($target.is('fieldset')) {
$target.removeAttr('required aria-required');
}
}
});
$document.on('state:readonly', function (e) {
if (e.trigger && $(e.target).isWebform()) {
$(e.target).prop('readonly', e.value).closest('.js-form-item, .js-form-wrapper').toggleClass('webform-readonly', e.value).find('input, textarea').prop('readonly', e.value);
}
});
$document.on('state:visible state:visible-slide', function (e) {
if (e.trigger && $(e.target).isWebform()) {
if (e.value) {
$(':input', e.target).addBack().each(function () {
restoreValueAndRequired(this);
triggerEventHandlers(this);
});
}
else {
// @see https://www.sitepoint.com/jquery-function-clear-form-data/
$(':input', e.target).andSelf().each(function () {
var $input = $(this);
$(':input', e.target).addBack().each(function () {
backupValueAndRequired(this);
clearValueAndRequired(this);
triggerEventHandlers(this);
});
}
else {
$(':input', e.target).andSelf().each(function () {
restoreValueAndRequired(this);
triggerEventHandlers(this);
});
}
});
$document.on('state:disabled', function (e) {
if (e.trigger) {
$(e.target).trigger('webform:disabled')
.find('select, input, textarea').trigger('webform:disabled');
}
});
}
});
$document.bind('state:visible-slide', function (e) {
if (e.trigger && $(e.target).isWebform()) {
var effect = e.value ? 'slideDown' : 'slideUp';
var duration = Drupal.webform.states[effect].duration;
$(e.target).closest('.js-form-item, .js-form-submit, .js-form-wrapper')[effect](duration);
}
});
Drupal.states.State.aliases['invisible-slide'] = '!visible-slide';
$document.on('state:disabled', function (e) {
if (e.trigger && $(e.target).isWebform()) {
// Make sure disabled property is set before triggering webform:disabled.
// Copied from: core/misc/states.js
$(e.target)
.prop('disabled', e.value)
.closest('.js-form-item, .js-form-submit, .js-form-wrapper').toggleClass('form-disabled', e.value)
.find('select, input, textarea, button').prop('disabled', e.value);
// Trigger webform:disabled.
$(e.target).trigger('webform:disabled')
.find('select, input, textarea, button').trigger('webform:disabled');
}
});
/**
* Trigger custom HTML5 multiple checkboxes validation.
*
* @see https://stackoverflow.com/a/37825072/145846
*/
function checkboxRequiredhandler() {
var $checkboxes = $(this).closest('.js-webform-type-checkboxes, .js-form-type-webform-checkboxes-other').find('input[type="checkbox"]');
if ($checkboxes.is(':checked')) {
$checkboxes.removeAttr('required aria-required');
}
else {
$checkboxes.attr({'required': 'required', 'aria-required': 'true'});
}
}
/**
* Trigger an input's event handlers.
*
* @param input
* @param {element} input
* An input.
*/
function triggerEventHandlers(input) {
var $input = $(input);
var type = input.type;
var tag = input.tagName.toLowerCase(); // Normalize case.
if (type == 'checkbox' || type == 'radio') {
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')
.trigger('blur');
.trigger('change', extraParameters)
.trigger('blur', extraParameters);
}
else if (tag == 'select') {
else if (tag === 'select') {
$input
.trigger('change')
.trigger('blur');
.trigger('change', extraParameters)
.trigger('blur', extraParameters);
}
else if (type != 'submit' && type != 'button') {
else if (type !== 'submit' && type !== 'button' && type !== 'file') {
$input
.trigger('input')
.trigger('change')
.trigger('keydown')
.trigger('keyup')
.trigger('blur');
.trigger('input', extraParameters)
.trigger('change', extraParameters)
.trigger('keydown', extraParameters)
.trigger('keyup', extraParameters)
.trigger('blur', extraParameters);
}
}
/**
* Backup an input's current value and required attribute
*
* @param input
* @param {element} input
* An input.
*/
function backupValueAndRequired(input) {
@ -83,31 +280,32 @@
var tag = input.tagName.toLowerCase(); // Normalize case.
// Backup required.
if ($input.prop('required')) {
$input.data('webform-require', true);
if ($input.prop('required') && !$input.hasData('webform-required')) {
$input.data('webform-required', true);
}
// Backup value.
if (type == 'checkbox' || type == 'radio') {
$input.data('webform-value', $input.prop('checked'));
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);
}
}
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 input
* @param {element} input
* An input.
*/
function restoreValueAndRequired(input) {
@ -119,41 +317,52 @@
var type = input.type;
var tag = input.tagName.toLowerCase(); // Normalize case.
if (type == 'checkbox' || type == 'radio') {
$input.prop('checked', value)
if (type === 'checkbox' || type === 'radio') {
$input.prop('checked', value);
}
else if (tag == 'select') {
else if (tag === 'select') {
$.each(value, function (i, option_value) {
$input.find("option[value='" + option_value + "']").prop("selected", true);
$input.find("option[value='" + option_value + "']").prop('selected', true);
});
}
else if (type != 'submit' && type != 'button') {
else if (type !== 'submit' && type !== 'button') {
input.value = value;
}
$input.removeData('webform-value');
}
// Restore required.
if ($input.data('webform-required')) {
$input.prop('required', TRUE);
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 input
* @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') {
if (type === 'checkbox' || type === 'radio') {
$input.prop('checked', false);
}
else if (tag == 'select') {
else if (tag === 'select') {
if ($input.find('option[value=""]').length) {
$input.val('');
}
@ -161,8 +370,8 @@
input.selectedIndex = -1;
}
}
else if (type != 'submit' && type != 'button') {
input.value = (type == 'color') ? '#000000' : '';
else if (type !== 'submit' && type !== 'button') {
input.value = (type === 'color') ? '#000000' : '';
}
// Clear required.

View file

@ -1,6 +1,6 @@
/**
* @file
* Javascript behaviors for jQuery UI tooltip integration.
* JavaScript behaviors for jQuery UI tooltip integration.
*
* Please Note:
* jQuery UI's tooltip implementation is not very responsive or adaptive.
@ -12,6 +12,31 @@
'use strict';
var tooltipDefaultOptions = {
// @see https://stackoverflow.com/questions/18231315/jquery-ui-tooltip-html-with-links
show: {delay: 100},
close: function (event, ui) {
ui.tooltip.hover(
function () {
$(this).stop(true).fadeTo(400, 1);
},
function () {
$(this).fadeOut('400', function () {
$(this).remove();
});
});
}
};
// @see http://api.jqueryui.com/tooltip/
Drupal.webform = Drupal.webform || {};
Drupal.webform.tooltipElement = Drupal.webform.tooltipElement || {};
Drupal.webform.tooltipElement.options = Drupal.webform.tooltipElement.options || tooltipDefaultOptions;
Drupal.webform.tooltipLink = Drupal.webform.tooltipLink || {};
Drupal.webform.tooltipLink.options = Drupal.webform.tooltipLink.options || tooltipDefaultOptions;
/**
* Initialize jQuery UI tooltip element support.
*
@ -21,11 +46,36 @@
attach: function (context) {
$(context).find('.js-webform-tooltip-element').once('webform-tooltip-element').each(function () {
var $element = $(this);
var $description = $element.children('.description.visually-hidden');
$element.tooltip({
items: ':input',
// Checkboxes, radios, buttons, toggles, etc… use fieldsets.
// @see \Drupal\webform\Plugin\WebformElement\OptionsBase::prepare
var $description;
if ($element.is('fieldset')) {
$description = $element.find('> .fieldset-wrapper > .description > .webform-element-description.visually-hidden');
}
else {
$description = $element.find('> .description > .webform-element-description.visually-hidden');
}
var has_visible_input = $element.find(':input:not([type=hidden])').length;
var has_checkboxes_or_radios = $element.find(':checkbox, :radio').length;
var is_composite = $element.hasClass('form-composite');
var is_custom = $element.is('.js-form-type-webform-signature, .js-form-type-webform-image-select, .js-form-type-webform-mapping, .js-form-type-webform-rating, .js-form-type-datelist, .js-form-type-datetime');
var items;
if (has_visible_input && !has_checkboxes_or_radios && !is_composite && !is_custom) {
items = ':input';
}
else {
items = $element;
}
var options = $.extend({
items: items,
content: $description.html()
});
}, Drupal.webform.tooltipElement.options);
$element.tooltip(options);
});
}
};
@ -37,8 +87,12 @@
*/
Drupal.behaviors.webformTooltipLink = {
attach: function (context) {
$(context).find('a.js-webform-tooltip-link').once('webform-tooltip-link').each(function () {
$(this).tooltip();
$(context).find('.js-webform-tooltip-link').once('webform-tooltip-link').each(function () {
var $link = $(this);
var options = $.extend({}, Drupal.webform.tooltipLink.options);
$link.tooltip(options);
});
}
};

View file

@ -0,0 +1,61 @@
/**
* @file
* JavaScript behaviors for webform wizard.
*/
(function ($, Drupal) {
'use strict';
/**
* Link the wizard's previous pages.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Links the wizard's previous pages.
*/
Drupal.behaviors.webformWizardPagesLink = {
attach: function (context) {
$('.js-webform-wizard-pages-links', context).once('webform-wizard-pages-links').each(function () {
var $pages = $(this);
var $form = $pages.closest('form');
var hasProgressLink = $pages.data('wizard-progress-link');
var hasPreviewLink = $pages.data('wizard-preview-link');
$pages.find('.js-webform-wizard-pages-link').each(function () {
var $button = $(this);
var title = $button.attr('title');
var page = $button.data('webform-page');
// Link progress marker and title.
if (hasProgressLink) {
var $progress = $form.find('.webform-progress [data-webform-page="' + page + '"]');
$progress.find('.progress-marker, .progress-title')
.attr({
'role': 'link',
'title': title,
'aria-label': title
})
.click(function () {
$button.click();
});
// Only allow the marker to be tabbable.
$progress.find('.progress-marker').attr('tabindex', 0);
}
// Move button to preview page div container with [data-webform-page].
// @see \Drupal\webform\Plugin\WebformElement\WebformWizardPage::formatHtmlItem
if (hasPreviewLink) {
$form
.find('.webform-preview [data-webform-page="' + page + '"]')
.append($button)
.show();
}
});
});
}
};
})(jQuery, Drupal);

View file

@ -0,0 +1,41 @@
/**
* @file
* JavaScript behaviors for webform wizard.
*/
(function ($, Drupal) {
'use strict';
/**
* Tracks the wizard's current page in the URL.
*
* @type {Drupal~behavior}
*
* @prop {Drupal~behaviorAttach} attach
* Tracks the wizard's current page in the URL.
*/
Drupal.behaviors.webformWizardTrackPage = {
attach: function (context) {
// Make sure on page load or Ajax refresh the location ?page is correct
// since conditional logic can skip pages.
// Note: window.history is only supported by IE 10+ and all other browsers.
if (window.history && history.replaceState) {
$('form[data-webform-wizard-current-page]', context).once('webform-wizard-current-page').each(function () {
var page = $(this).attr('data-webform-wizard-current-page');
history.replaceState(null, null, window.location.toString().replace(/\?.+$/, '') + '?page=' + page);
});
}
// When paging next and back update the URL so that Drupal knows what
// the expected page name or index is going to be.
// NOTE: If conditional wizard page logic is configured the
// expected page name or index may not be accurate.
$(':button[data-webform-wizard-page], :submit[data-webform-wizard-page]', context).once('webform-wizard-page').on('click', function () {
var page = $(this).attr('data-webform-wizard-page');
this.form.action = this.form.action.replace(/\?.+$/, '') + '?page=' + page;
});
}
};
})(jQuery, Drupal);