// $Id: form_builder.js,v 1.17 2009/06/20 21:18:42 quicksketch Exp $
/**
* @file form_builder.js
* Provide enhancements to the form building user interface.
*/
Drupal.behaviors.formBuilderElement = function(context) {
var $wrappers = $('div.form-builder-wrapper', context);
var $elements = $('div.form-builder-element', context);
// If the context itself is a wrapper, add it to the list.
if ($(context).is('div.form-builder-wrapper')) {
$wrappers = $wrappers.add(context);
}
// Add over effect on rollover.
// The .hover() method is not used to avoid issues with nested hovers.
$wrappers.not('div.form-builder-empty-placeholder')
.bind('mouseover', Drupal.formBuilder.addHover)
.bind('mouseout', Drupal.formBuilder.removeHover);
// Add AJAX to edit links.
$wrappers.find('span.form-builder-links a.configure').click(Drupal.formBuilder.editField);
// Add AJAX to remove links.
$wrappers.find('span.form-builder-links a.remove').click(Drupal.formBuilder.editField);
// Add AJAX to entire field for easy editing.
$elements.each(function() {
if ($(this).children('fieldset.form-builder-fieldset').size() == 0) {
var link = $(this).parents('div.form-builder-wrapper:first').find('a.configure').get(0);
if (link) {
$(this).click(Drupal.formBuilder.clickField).addClass('form-builder-clickable');
$(this).find('div.form-builder-element label').click(Drupal.formBuilder.clickField);
}
else {
$(this).addClass('form-builder-draggable');
}
}
});
// Disable field functionality on click.
$elements.find('input, textarea').bind('mousedown', Drupal.formBuilder.disableField);
};
/**
* Behavior to disable preview fields and instead open up the configuration.
*/
Drupal.behaviors.formBuilderFields = function(context) {
// Bind a function to all elements to update the preview on change.
var $configureForm = $('#form-builder-field-configure');
$configureForm.find('input, textarea, select')
.filter(':not(.form-builder-field-change)')
.addClass('form-builder-field-change')
.bind('change', Drupal.formBuilder.elementPendingChange);
$configureForm.find('input.form-text, textarea')
.filter(':not(.form-builder-field-keyup)')
.addClass('form-builder-field-keyup')
.bind('keyup', Drupal.formBuilder.elementPendingChange);
}
/**
* Behavior for the entire form builder. Add drag and drop to elements.
*/
Drupal.behaviors.formBuilder = function(context) {
$('#form-builder', context).sortable({
items: 'div.form-builder-wrapper',
handle: 'div.form-builder-title-bar, div.form-builder-element',
axis: 'y',
opacity: 0.8,
forcePlaceholderSize: true,
scroll: true,
scrollSensitivity: 50,
distance: 4, // Pixels before dragging starts.
appendTo: 'body',
helper: createHelper,
sort: Drupal.formBuilder.elementIndent, // Called on drag.
start: Drupal.formBuilder.startDrag,
stop: Drupal.formBuilder.stopDrag,
change: Drupal.formBuilder.checkFieldsets
});
// This helper function is needed to make the appendTo option take effect.
function createHelper(e, $el) {
return $el.clone().get(0);
}
};
/**
* Behavior that renders fieldsets as tabs within the field configuration form.
*/
Drupal.behaviors.formBuilderTabs = function(context) {
var $fieldsets = $('fieldset.form-builder-group:not(.form-builer-tabs-processed)', context);
var $close = $('' + Drupal.t('Close') + '');
var $tabs;
var tabs = '';
// Convert fieldsets to tabs.
tabs = '
';
// Add the new tabs to the page.
$tabs = $(tabs);
$fieldsets.filter(':first').before($close).before($tabs);
// Hide all the fieldsets except the first.
$fieldsets.filter(':not(:first)').css('display', 'none');
$tabs.find('li:first').addClass('active').click(Drupal.formBuilder.clickCancel);
// Enable tab switching by clicking on each tab.
$tabs.find('li:not(.close)').each(function(index) {
$(this).click(function() {
$fieldsets.filter(':visible').css('display', 'none');
$fieldsets.eq(index).css('display', 'block');
$tabs.find('li.active').removeClass('active').unbind('click', Drupal.formBuilder.clickCancel);
$(this).addClass('active').click(Drupal.formBuilder.clickCancel);
Drupal.formBuilder.fixTableDragTabs($fieldsets.eq(index).get(0));
});
});
$close.click(Drupal.formBuilder.clickCancel);
// Add guard class.
$fieldsets.addClass('form-builer-tabs-processed');
};
/**
* Submit the delete form via AJAX or close the form with the cancel link.
*/
Drupal.behaviors.formBuilderDeleteConfirmation = function(context) {
$confirmForm = $('form.confirmation');
if ($confirmForm.size()) {
$confirmForm.submit(Drupal.formBuilder.deleteField);
$confirmForm.find('a').click(Drupal.formBuilder.clickCancel);
}
}
/**
* Keeps record of if a mouse button is pressed.
*/
Drupal.behaviors.formBuilderMousePress = function(context) {
if (context == document) {
$('body').mousedown(function() { Drupal.formBuilder.mousePressed = 1; });
$('body').mouseup(function() { Drupal.formBuilder.mousePressed = 0; });
}
}
/**
* Scrolls the add new field block with the window.
*/
Drupal.behaviors.formBuilderBlockScroll = function(context) {
var $list = $('ul.form-builder-fields', context);
if ($list.size()) {
var $block = $list.parents('div.block:first').css('position', 'relative');
var blockScrollStart = $block.offset().top;
function blockScroll() {
// Do not move the palette while dragging a field.
if (Drupal.formBuilder.activeDragUi) {
return;
}
var windowOffset = $(window).scrollTop();
var blockHeight = $block.height();
var formBuilderHeight = $('#form-builder').height();
if (windowOffset - blockScrollStart > 0) {
// Do not scroll beyond the bottom of the editing area.
var newTop = Math.min(windowOffset - blockScrollStart + 20, formBuilderHeight - blockHeight);
$block.animate({ top: (newTop + 'px') }, 'fast');
}
else {
$block.animate({ top: '0px' }, 'fast');
}
}
var timeout = false;
function scrollTimeout() {
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(blockScroll, 100);
}
$(window).scroll(scrollTimeout);
}
}
/**
* Behavior for the Add a field block.
* @param {Object} context
*/
Drupal.behaviors.formBuilderNewField = function(context) {
var $list = $('ul.form-builder-fields', context);
if ($list.size()) {
// Allow items to be copied from the list of new fields.
$list.children('li:not(.ui-draggable)').draggable({
opacity: 0.8,
helper: 'clone',
scroll: true,
scrollSensitivity: 50,
containment: 'body',
connectToSortable: ['#form-builder'],
start: Drupal.formBuilder.startPaletteDrag,
stop: Drupal.formBuilder.stopPaletteDrag,
change: Drupal.formBuilder.checkFieldsets
});
}
}
Drupal.formBuilder = {
// Variable to prevent multiple requests.
updatingElement: false,
// Variables to allow delayed updates on textfields and textareas.
updateDelayElement: false,
updateDelay: false,
// Variable holding the actively edited element (if any).
activeElement: false,
// Variable holding the active drag object (if any).
activeDragUi: false,
// Variable of the time of the last update, used to prevent old data from
// replacing newer updates.
lastUpdateTime: 0,
// Status of mouse click.
mousePressed: 0
};
/**
* Event callback for mouseover of fields. Adds hover class.
*/
Drupal.formBuilder.addHover = function() {
// Do not add hover effect while dragging over other fields.
if (!Drupal.formBuilder.activeDragUi && !Drupal.formBuilder.mousePressed) {
if ($(this).find('div.form-builder-hover').size() == 0) {
$(this).addClass('form-builder-hover');
}
}
}
/**
* Event callback for mouseout of fields. Removes hover class.
*/
Drupal.formBuilder.removeHover = function() {
// Do not add hover effect while dragging over other fields.
if (!Drupal.formBuilder.activeDragUi && !Drupal.formBuilder.mousePressed) {
$(this).removeClass('form-builder-hover');
}
}
/**
* Click handler for fields.
*
* Note this is applied to both the entire field and to the labels within the
* field, as they have special browser behavior that needs to be overridden.
*/
Drupal.formBuilder.clickField = function(e) {
// Allow select lists to be clicked on without opening the edit options.
if ($(e.target).is('select')) {
return;
}
var link = $(this).parents('div.form-builder-wrapper:first').find('a.configure').get(0);
Drupal.formBuilder.editField.apply(link);
return false;
}
/**
* Mousedown event on element previews.
*/
Drupal.formBuilder.disableField = function(e) {
return false;
}
/**
* Load the edit form from the server.
*/
Drupal.formBuilder.editField = function() {
var element = $(this).parents('div.form-builder-wrapper').get(0);
var link = this;
// Prevent duplicate clicks from taking effect if already handling a click.
if (Drupal.formBuilder.updatingElement) {
return false;
}
// If clicking on the link a second time, close the form instead of open.
if (element == Drupal.formBuilder.activeElement && link == Drupal.formBuilder.activeLink) {
$(link).addClass('progress');
Drupal.formBuilder.closeActive(function() {
$(link).removeClass('progress');
});
Drupal.formBuilder.unsetActive();
return false;
}
var getForm = function() {
$.ajax({
url: link.href,
type: 'GET',
dataType: 'json',
data: 'js=1',
success: Drupal.formBuilder.displayForm,
});
};
$(link).addClass('progress');
Drupal.formBuilder.updatingElement = true;
Drupal.formBuilder.closeActive(getForm);
Drupal.formBuilder.setActive(element, link);
return false;
};
/**
* Click handler for deleting a field.
*/
Drupal.formBuilder.deleteField = function() {
$(this).parents('div.form-builder-wrapper:first').animate({ height: 'hide', opacity: 'hide' }, 'normal', function() {
// If this is a unique field, show the field in the palette again.
var elementId = $(this).find('div.form-builder-element').attr('id');
$('ul.form-builder-fields').find('li.' + elementId).show('slow');
// Remove the field from the form.
$(this).remove();
// Check for empty fieldsets.
Drupal.formBuilder.checkFieldsets(null, null, true);
});
}
Drupal.formBuilder.clickCancel = function() {
Drupal.formBuilder.closeActive();
Drupal.formBuilder.unsetActive();
return false;
}
/**
* Display the edit form from the server.
*/
Drupal.formBuilder.displayForm = function(response) {
var $preview = $('#form-builder-element-' + response.elementId);
var $form = $(response.html).insertAfter($preview).css('display', 'none');
Drupal.attachBehaviors($form.parent().get(0));
$form
// Add the ajaxForm behavior to the new form.
.ajaxForm()
// Using the 'data' $.ajaxForm property doesn't seem to work.
// Manually add a hidden element to pass additional data on submit.
.prepend('');
$form.slideDown(function() {
$form.parents('div.form-builder-wrapper:first').find('a.progress').removeClass('progress');
});
//Drupal.unfreezeHeight();
Drupal.formBuilder.updatingElement = false;
};
/**
* Upon changing a field, submit via AJAX to the server.
*/
Drupal.formBuilder.elementChange = function() {
if (!Drupal.formBuilder.updatingElement) {
$(this).parents('form:first').ajaxSubmit({
success: Drupal.formBuilder.updateElement,
dataType: 'json',
});
}
// Clear any pending updates until further changes are made.
if (Drupal.formBuilder.updateDelay) {
clearTimeout(Drupal.formBuilder.updateDelay);
}
Drupal.formBuilder.updatingElement = true;
};
/**
* Update a field after a delay.
*
* Similar to immediately changing a field, this field as pending changes that
* will be updated after a delay. This includes textareas and textfields in
* which updating continuously would be a strain the server and actually slow
* down responsiveness.
*/
Drupal.formBuilder.elementPendingChange = function(e) {
// Only operate on "normal" keys, excluding special function keys.
// http://protocolsofmatrix.blogspot.com/2007/09/javascript-keycode-reference-table-for.html
if (e.type == 'keyup' && !(
e.keyCode >= 48 && e.keyCode <= 90 || // 0-9, A-Z.
e.keyCode >= 93 && e.keyCode <= 111 || // Number pad.
e.keyCode >= 186 && e.keyCode <= 222 || // Symbols.
e.keyCode == 8) // Backspace.
) {
return;
}
if (Drupal.formBuilder.updateDelay) {
clearTimeout(Drupal.formBuilder.updateDelay);
}
Drupal.formBuilder.updateDelayElement = this;
Drupal.formBuilder.updateDelay = setTimeout("Drupal.formBuilder.elementChange.apply(Drupal.formBuilder.updateDelayElement, [true])", 500);
};
/**
* After submitting the change to the server, display the updated element.
*/
Drupal.formBuilder.updateElement = function(response) {
var $configureForm = $('#form-builder-field-configure');
// Do not let older requests replace newer updates.
if (response.time < Drupal.formBuilder.lastUpdateTime) {
return;
}
else {
Drupal.formBuilder.lastUpdateTime = response.time;
}
// Set the error class on fields.
$configureForm.find('.error').removeClass('error');
if (response.errors) {
for (var elementName in response.errors) {
elementName = elementName.replace(/([a-z0-9_]+)\](.*)/, '$1$2]');
$configureForm.find('[name=' + elementName + ']').addClass('error');
}
}
// Display messages, if any.
$configureForm.find('.messages').remove();
if (response.messages) {
$configureForm.find('fieldset:visible:first').prepend(response.messages);
}
// Do not update the element if errors were received.
if (!response.errors) {
var $exisiting = $('#form-builder-element-' + response.elementId);
var $new = $(response.html).find('div.form-builder-element:first');
$exisiting.replaceWith($new);
// Expand root level fieldsets after updating to prevent them from closing
// after every update.
$new.children('fieldset.collapsible').removeClass('collapsed');
Drupal.attachBehaviors($new.parent().get(0));
}
// Set the variable stating we're done updating.
Drupal.formBuilder.updatingElement = false;
};
/**
* When adding a new field, remove the placeholder and insert the new element.
*/
Drupal.formBuilder.addElement = function(response) {
// This is very similar to the update element callback, only we replace the
// entire wrapper instead of just the element.
var $exisiting = $('#form-builder-element-' + response.elementId).parent();
var $new = $(response.html).find('div.form-builder-element:first').parent();
$exisiting.replaceWith($new);
Drupal.attachBehaviors($new.get(0));
// Set the variable stating we're done updating.
Drupal.formBuilder.updatingElement = false;
// Insert the new position form containing the new element.
$('#form-builder-positions').replaceWith(response.positionForm);
// Submit the new positions form to save the new element position.
Drupal.formBuilder.updateElementPosition($new.get(0));
};
/**
* Given an element, update it's position (weight and parent) on the server.
*/
Drupal.formBuilder.updateElementPosition = function(element) {
// Update weights of all children within this element's parent.
$(element).parent().children('div.form-builder-wrapper').each(function(index) {
var child_id = $(this).children('div.form-builder-element:first').attr('id');
$('#form-builder-positions input.form-builder-weight').filter('.' + child_id).val(index);
});
// Update this element's parent.
var $parent = $(element).parents('div.form-builder-element:first');
var parent_id = $parent.size() ? $parent.attr('id').replace(/form-builder-element-(.*)/, '$1') : 0;
var child_id = $(element).children('div.form-builder-element:first').attr('id');
$('#form-builder-positions input.form-builder-parent').filter('.' + child_id).val(parent_id);
// Submit the position form via AJAX to save the new weights and parents.
$('#form-builder-positions').ajaxSubmit();
}
/**
* Called when a field is about to be moved via Sortables.
*
* @param e
* The event object containing status information about the event.
* @param ui
* The jQuery Sortables object containing information about the sortable.
*/
Drupal.formBuilder.startDrag = function(e, ui) {
Drupal.formBuilder.activeDragUi = ui;
}
/**
* Called when a field has been moved via Sortables.
*
* @param e
* The event object containing status information about the event.
* @param ui
* The jQuery Sortables object containing information about the sortable.
*/
Drupal.formBuilder.stopDrag = function(e, ui){
var element = ui.item.get(0);
// If the element is a new field from the palette, update it with a real field.
if ($(element).is('.ui-draggable')) {
var name = 'new_' + new Date().getTime();
// If this is a "unique" element, its element ID is hard-coded.
if ($(element).is('.form-builder-unique')) {
name = element.className.replace(/^.*?form-builder-element-([a-z0-9_]+).*?$/, '$1');
}
var $ajaxPlaceholder = $('
' + Drupal.t('Please wait...') + '
');
$.ajax({
url: $(element).find('a').get(0).href,
type: 'GET',
dataType: 'json',
data: 'js=1&element_id=' + name,
success: Drupal.formBuilder.addElement,
});
$(element).replaceWith($ajaxPlaceholder);
Drupal.formBuilder.updatingElement = true;
}
// Update the positions (weights and parents) in the form cache.
else {
Drupal.formBuilder.updateElementPosition(element);
}
Drupal.formBuilder.activeDragUi = false;
// Scroll the palette into view.
$(window).scroll();
}
/**
* Called when a field is about to be moved from the new field palette.
*
* @param e
* The event object containing status information about the event.
* @param ui
* The jQuery Sortables object containing information about the sortable.
*/
Drupal.formBuilder.startPaletteDrag = function(e, ui) {
if ($(this).is('.form-builder-unique')) {
$(this).css('visibility', 'hidden');
}
Drupal.formBuilder.activeDragUi = ui;
}
/**
* Called after a field has been moved from the new field palette.
*
* @param e
* The event object containing status information about the event.
* @param ui
* The jQuery Sortables object containing information about the sortable.
*/
Drupal.formBuilder.stopPaletteDrag = function(e, ui) {
// If the activeDragUi is still set, we did not drop onto the form.
if (Drupal.formBuilder.activeDragUi) {
ui.helper.remove();
Drupal.formBuilder.activeDragUi = false;
$(this).css('visibility', '');
$(window).scroll();
}
// If dropped onto the form and a unique field, remove it from the palette.
else if ($(this).is('.form-builder-unique')){
$(this).animate({ height: '0', width: '0' }, function() {
$(this).css({ visibility: '', height: '', width: '', display: 'none' });
});
}
}
/**
* Update the indentation and width of elements as they move over fieldsets.
*
* This function is called on every mouse movement during a Sortables drag.
*
* @param e
* The event object containing status information about the event.
* @param ui
* The jQuery Sortables object containing information about the sortable.
*/
Drupal.formBuilder.elementIndent = function(e, ui) {
var placeholder = ui.placeholder.get(0);
var helper = ui.helper.get(0);
var item = ui.item.get(0);
// Do not affect the elements being dragged from the pallette.
if ($(item).is('li')) {
return;
}
// Turn on the placeholder item (which is in the final location) to take some stats.
$(placeholder).css('visibility', 'visible');
var difference = $(helper).width() - $(placeholder).width();
var offset = $(placeholder).offset().left;
$(placeholder).css('visibility', 'hidden');
// Adjust the helper to match the location and width of the real item.
var newWidth = $(helper).width() - difference;
$(helper).css('width', newWidth + 'px');
$(helper).css('left', offset + 'px');
}
/**
* Insert DIVs into empty fieldsets so that items can be dropped within them.
*
* This function is called every time an element changes positions during
* a Sortables drag and drop operation.
*
* @param e
* The event object containing status information about the event.
* @param ui
* The jQuery Sortables object containing information about the sortable.
* @param
*/
Drupal.formBuilder.checkFieldsets = function(e, ui, expand) {
var $fieldsets = $('#form-builder').find('div.form-builder-element > fieldset.form-builder-fieldset');
var emptyFieldsets = [];
// Remove all current fieldset placeholders.
$fieldsets.find('.ui-sortable-placeholder').siblings('div.form-builder-empty-placeholder').remove();
// Find all empty fieldsets.
$fieldsets.each(function() {
// Check for empty collapsible fieldsets.
if ($(this).children('div.fieldset-wrapper').size()) {
if ($(this).children('div.fieldset-wrapper').children(':not(.description):visible, .ui-sortable-placeholder').filter().size() == 0) {
emptyFieldsets.push(this);
}
}
// Check for empty normal fieldsets.
if ($(this).children(':not(legend, .description):visible, .ui-sortable-placeholder').size() == 0) {
emptyFieldsets.push(this);
}
});
// Add a placeholder DIV in the empty fieldsets.
$(emptyFieldsets).each(function() {
var wrapper = $(this).children('div.fieldset-wrapper').get(0) || this;
var $placeholder = $(Drupal.settings.formBuilder.emptyFieldset).css('display', 'none').appendTo(wrapper);
if (expand) {
$placeholder.slideDown();
}
else {
$placeholder.css('display', 'block');
}
});
$('#form-builder').sortable('refresh');
}
Drupal.formBuilder.setActive = function(element, link) {
Drupal.formBuilder.unsetActive();
Drupal.formBuilder.activeElement = element;
Drupal.formBuilder.activeLink = link;
$(Drupal.formBuilder.activeElement).addClass('form-builder-active');
};
Drupal.formBuilder.unsetActive = function() {
if (Drupal.formBuilder.activeElement) {
$(Drupal.formBuilder.activeElement).removeClass('form-builder-active');
Drupal.formBuilder.activeElement = false;
Drupal.formBuilder.activeLink = false;
}
}
Drupal.formBuilder.closeActive = function(callback) {
if (Drupal.formBuilder.activeElement) {
var $activeForm = $(Drupal.formBuilder.activeElement).find('form');
if ($activeForm.size()) {
Drupal.freezeHeight();
$activeForm.slideUp(function(){
$(this).remove();
if (callback) {
callback.call();
}
});
}
}
else if (callback) {
callback.call();
}
return false;
};
/**
* Work around for tabledrags within tabs. On load, if the tab was hidden the
* offsets cannot be calculated correctly. Recalculate and update the tableDrag.
*/
Drupal.formBuilder.fixTableDragTabs = function(context) {
if (Drupal.tableDrag && Drupal.tableDrag.length > 1) {
for (var n in Drupal.tableDrag) {
if (typeof(Drupal.tableDrag[n]) == 'object') {
var table = $('#' + n, context).get(0);
if (table) {
var indent = Drupal.theme('tableDragIndentation');
var testCell = $('tr.draggable:first td:first', table).prepend(indent).prepend(indent);
Drupal.tableDrag[n].indentAmount = $('.indentation', testCell).get(1).offsetLeft - $('.indentation', testCell).get(0).offsetLeft;
$('.indentation', testCell).slice(0, 2).remove();
}
}
}
}
}