Commit 25b9f686 authored by webchick's avatar webchick
Browse files

#544418 by merlinofchaos, sun, drewish, quicksketch, et al: Integrate CTools...

#544418 by merlinofchaos, sun, drewish, quicksketch, et al: Integrate CTools AJAX framework with Drupal to extend (and replace) existing ahah framework. Everything about AJAX/AHAH is more betterer now.
parent 55d94637
......@@ -3549,6 +3549,7 @@ function _drupal_bootstrap_full() {
require_once DRUPAL_ROOT . '/includes/form.inc';
require_once DRUPAL_ROOT . '/includes/mail.inc';
require_once DRUPAL_ROOT . '/includes/actions.inc';
require_once DRUPAL_ROOT . '/includes/ajax.inc';
// Set the Drupal custom error handler.
set_error_handler('_drupal_error_handler');
set_exception_handler('_drupal_exception_handler');
......
......@@ -1800,48 +1800,6 @@ function weight_value(&$form) {
}
}
/**
* Menu callback for AHAH callbacks through the #ahah['callback'] FAPI property.
*/
function form_ahah_callback() {
$form_state = form_state_defaults();
$form_build_id = $_POST['form_build_id'];
// Get the form from the cache.
$form = form_get_cache($form_build_id, $form_state);
if (!$form) {
// If $form cannot be loaded from the cache, the form_build_id in $_POST must
// be invalid, which means that someone performed a POST request onto
// system/ahah without actually viewing the concerned form in the browser.
// This is likely a hacking attempt as it never happens under normal
// circumstances, so we just do nothing.
exit;
}
// We will run some of the submit handlers so we need to disable redirecting.
$form['#redirect'] = FALSE;
// We need to process the form, prepare for that by setting a few internals
// variables.
$form_state['input'] = $_POST;
$form_state['args'] = $form['#args'];
$form_id = $form['#form_id'];
// Build, validate and if possible, submit the form.
drupal_process_form($form_id, $form, $form_state);
// This call recreates the form relying solely on the form_state that the
// drupal_process_form set up.
$form = drupal_rebuild_form($form_id, $form_state, $form_build_id);
// Get the callback function from the clicked button.
$callback = $form_state['clicked_button']['#ahah']['callback'];
if (drupal_function_exists($callback)) {
$callback($form, $form_state);
}
}
/**
* Roll out a single radios element to a list of radios,
* using the options array as index.
......@@ -1861,7 +1819,7 @@ function form_process_radios($element) {
'#attributes' => $element['#attributes'],
'#parents' => $element['#parents'],
'#id' => form_clean_id('edit-' . implode('-', $parents_for_id)),
'#ahah' => isset($element['#ahah']) ? $element['#ahah'] : NULL,
'#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
);
}
}
......@@ -1970,91 +1928,6 @@ function theme_text_format_wrapper($element) {
return $output;
}
/**
* Add AHAH information about a form element to the page to communicate with
* javascript. If #ahah[path] is set on an element, this additional javascript is
* added to the page header to attach the AHAH behaviors. See ahah.js for more
* information.
*
* @param $element
* An associative array containing the properties of the element.
* Properties used: ahah_event, ahah_path, ahah_wrapper, ahah_parameters,
* ahah_effect.
* @return
* None. Additional code is added to the header of the page using
* drupal_add_js.
*/
function form_process_ahah($element) {
$js_added = &drupal_static(__FUNCTION__, array());
// Add a reasonable default event handler if none specified.
if (isset($element['#ahah']) && !isset($element['#ahah']['event'])) {
switch ($element['#type']) {
case 'submit':
case 'button':
case 'image_button':
// Use the mousedown instead of the click event because form
// submission via pressing the enter key triggers a click event on
// submit inputs, inappropriately triggering AHAH behaviors.
$element['#ahah']['event'] = 'mousedown';
// Attach an additional event handler so that AHAH behaviors
// can be triggered still via keyboard input.
$element['#ahah']['keypress'] = TRUE;
break;
case 'password':
case 'textfield':
case 'textarea':
$element['#ahah']['event'] = 'blur';
break;
case 'radio':
case 'checkbox':
case 'select':
$element['#ahah']['event'] = 'change';
break;
default:
return $element;
}
}
// Adding the same javascript settings twice will cause a recursion error,
// we avoid the problem by checking if the javascript has already been added.
if ((isset($element['#ahah']['callback']) || isset($element['#ahah']['path'])) && isset($element['#ahah']['event']) && !isset($js_added[$element['#id']])) {
drupal_add_library('system', 'form');
drupal_add_js('misc/ahah.js');
$ahah_binding = array(
'url' => isset($element['#ahah']['callback']) ? url('system/ahah') : url($element['#ahah']['path']),
'event' => $element['#ahah']['event'],
'keypress' => empty($element['#ahah']['keypress']) ? NULL : $element['#ahah']['keypress'],
'wrapper' => empty($element['#ahah']['wrapper']) ? NULL : $element['#ahah']['wrapper'],
'selector' => empty($element['#ahah']['selector']) ? '#' . $element['#id'] : $element['#ahah']['selector'],
'effect' => empty($element['#ahah']['effect']) ? 'none' : $element['#ahah']['effect'],
'method' => empty($element['#ahah']['method']) ? 'replace' : $element['#ahah']['method'],
'progress' => empty($element['#ahah']['progress']) ? array('type' => 'throbber') : $element['#ahah']['progress'],
'button' => isset($element['#executes_submit_callback']) ? array($element['#name'] => $element['#value']) : FALSE,
);
// Convert a simple #ahah[progress] type string into an array.
if (is_string($ahah_binding['progress'])) {
$ahah_binding['progress'] = array('type' => $ahah_binding['progress']);
}
// Change progress path to a full url.
if (isset($ahah_binding['progress']['path'])) {
$ahah_binding['progress']['url'] = url($ahah_binding['progress']['path']);
}
// Add progress.js if we're doing a bar display.
if ($ahah_binding['progress']['type'] == 'bar') {
drupal_add_js('misc/progress.js', array('cache' => FALSE));
}
drupal_add_js(array('ahah' => array($element['#id'] => $ahah_binding)), 'setting');
$js_added[$element['#id']] = TRUE;
$element['#cache'] = TRUE;
}
return $element;
}
/**
* Format a checkbox.
*
......@@ -2132,7 +2005,7 @@ function form_process_checkboxes($element) {
'#return_value' => $key,
'#default_value' => isset($value[$key]),
'#attributes' => $element['#attributes'],
'#ahah' => isset($element['#ahah']) ? $element['#ahah'] : NULL,
'#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
);
}
}
......@@ -2245,7 +2118,7 @@ function form_process_tableselect($element) {
'#return_value' => $key,
'#default_value' => isset($value[$key]),
'#attributes' => $element['#attributes'],
'#ahah' => isset($element['#ahah']) ? $element['#ahah'] : NULL,
'#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
);
}
else {
......@@ -2260,7 +2133,7 @@ function form_process_tableselect($element) {
'#attributes' => $element['#attributes'],
'#parents' => $element['#parents'],
'#id' => form_clean_id('edit-' . implode('-', $parents_for_id)),
'#ahah' => isset($element['#ahah']) ? $element['#ahah'] : NULL,
'#ajax' => isset($element['#ajax']) ? $element['#ajax'] : NULL,
);
}
}
......
// $Id$
(function ($) {
/**
* Provides AJAX-like page updating via AHAH (Asynchronous HTML and HTTP).
*
* AHAH is a method of making a request via Javascript while viewing an HTML
* page. The request returns a small chunk of HTML, which is then directly
* injected into the page.
*
* Drupal uses this file to enhance form elements with #ahah[path] and
* #ahah[wrapper] properties. If set, this file will automatically be included
* to provide AHAH capabilities.
*/
Drupal.ahah = Drupal.ahah || {};
/**
* Attaches the ahah behavior to each ahah form element.
*/
Drupal.behaviors.ahah = {
attach: function (context, settings) {
for (var base in settings.ahah) {
if (!$('#' + base + '.ahah-processed').size()) {
var element_settings = settings.ahah[base];
$(element_settings.selector).each(function () {
element_settings.element = this;
Drupal.ahah[base] = new Drupal.ahah(base, element_settings);
});
$('#' + base).addClass('ahah-processed');
}
}
}
};
/**
* AHAH object.
*
* All AHAH objects on a page are accessible through the global Drupal.ahah object
* and are keyed by the submit button's ID. You can access them from your module's
* JavaScript file to override properties or functions.
* For example, if your AHAH enabled button has the ID 'edit-submit', you can
* redefine the function that is called to insert the new content like this
* (inside a Drupal.behaviors attach block):
* @code
* Drupal.behaviors.myCustomAhahStuff = {
* attach: function(context, settings) {
* Drupal.ahah['edit-submit'].insertNewContent = function(response, status) {
* new_content = $(response.data);
* $('#my-wrapper').append(new_content);
* alert('New content was appended to #my-wrapper');
* }
* }
* };
* @endcode
*/
Drupal.ahah = function (base, element_settings) {
// Set the properties for this object.
this.element = element_settings.element;
this.selector = element_settings.selector;
this.event = element_settings.event;
this.keypress = element_settings.keypress;
this.url = element_settings.url;
this.wrapper = '#' + element_settings.wrapper;
this.effect = element_settings.effect;
this.method = element_settings.method;
this.progress = element_settings.progress;
this.button = element_settings.button || { };
if (this.effect == 'none') {
this.showEffect = 'show';
this.hideEffect = 'hide';
this.showSpeed = '';
}
else if (this.effect == 'fade') {
this.showEffect = 'fadeIn';
this.hideEffect = 'fadeOut';
this.showSpeed = 'slow';
}
else {
this.showEffect = this.effect + 'Toggle';
this.hideEffect = this.effect + 'Toggle';
this.showSpeed = 'slow';
}
// Record the form action and target, needed for iFrame file uploads.
var form = $(this.element).parents('form');
this.form_action = form.attr('action');
this.form_target = form.attr('target');
this.form_encattr = form.attr('encattr');
// Set the options for the ajaxSubmit function.
// The 'this' variable will not persist inside of the options object.
var ahah = this;
var options = {
url: ahah.url,
data: ahah.button,
beforeSubmit: function (form_values, element_settings, options) {
return ahah.beforeSubmit(form_values, element_settings, options);
},
success: function (response, status) {
// Sanity check for browser support (object expected).
// When using iFrame uploads, responses must be returned as a string.
if (typeof response == 'string') {
response = Drupal.parseJson(response);
}
return ahah.success(response, status);
},
complete: function (response, status) {
if (status == 'error' || status == 'parsererror') {
return ahah.error(response, ahah.url);
}
},
dataType: 'json',
type: 'POST'
};
// Bind the ajaxSubmit function to the element event.
$(element_settings.element).bind(element_settings.event, function () {
$(element_settings.element).parents('form').ajaxSubmit(options);
return false;
});
// If necessary, enable keyboard submission so that AHAH behaviors
// can be triggered through keyboard input as well as e.g. a mousedown
// action.
if (element_settings.keypress) {
$(element_settings.element).keypress(function (event) {
// Detect enter key.
if (event.keyCode == 13) {
$(element_settings.element).trigger(element_settings.event);
return false;
}
});
}
};
/**
* Handler for the form redirection submission.
*/
Drupal.ahah.prototype.beforeSubmit = function (form_values, element, options) {
// Disable the element that received the change.
$(this.element).addClass('progress-disabled').attr('disabled', true);
// Insert progressbar or throbber.
if (this.progress.type == 'bar') {
var progressBar = new Drupal.progressBar('ahah-progress-' + this.element.id, eval(this.progress.update_callback), this.progress.method, eval(this.progress.error_callback));
if (this.progress.message) {
progressBar.setProgress(-1, this.progress.message);
}
if (this.progress.url) {
progressBar.startMonitoring(this.progress.url, this.progress.interval || 1500);
}
this.progress.element = $(progressBar.element).addClass('ahah-progress ahah-progress-bar');
this.progress.object = progressBar;
$(this.element).after(this.progress.element);
}
else if (this.progress.type == 'throbber') {
this.progress.element = $('<div class="ahah-progress ahah-progress-throbber"><div class="throbber">&nbsp;</div></div>');
if (this.progress.message) {
$('.throbber', this.progress.element).after('<div class="message">' + this.progress.message + '</div>');
}
$(this.element).after(this.progress.element);
}
};
/**
* Handler for the form redirection completion.
*/
Drupal.ahah.prototype.success = function (response, status) {
var form = $(this.element).parents('form');
// Restore the previous action and target to the form.
form.attr('action', this.form_action);
this.form_target ? form.attr('target', this.form_target) : form.removeAttr('target');
this.form_encattr ? form.attr('target', this.form_encattr) : form.removeAttr('encattr');
// Remove the progress element.
if (this.progress.element) {
$(this.progress.element).remove();
}
if (this.progress.object) {
this.progress.object.stopMonitoring();
}
$(this.element).removeClass('progress-disabled').attr('disabled', false);
Drupal.freezeHeight();
// Call the insertNewContent handler to insert the new content into the page.
this.insertNewContent(response, status);
Drupal.unfreezeHeight();
};
/**
* Handler to insert the new content into the page.
*/
Drupal.ahah.prototype.insertNewContent = function (response, status) {
var wrapper = $(this.wrapper);
// Manually insert HTML into the jQuery object, using $() directly crashes
// Safari with long string lengths. http://dev.jquery.com/ticket/1152
var new_content = $('<div></div>').html(response.data);
// Add the new content to the page.
if (this.method == 'replace') {
wrapper.empty().append(new_content);
}
else {
wrapper[this.method](new_content);
}
// Immediately hide the new content if we're using any effects.
if (this.showEffect != 'show') {
new_content.hide();
}
// Determine what effect use and what content will receive the effect, then
// show the new content.
if ($('.ahah-new-content', new_content).size() > 0) {
$('.ahah-new-content', new_content).hide();
new_content.show();
$('.ahah-new-content', new_content)[this.showEffect](this.showSpeed);
}
else if (this.showEffect != 'show') {
new_content[this.showEffect](this.showSpeed);
}
// Attach all javascript behaviors to the new content, if it was successfully
// added to the page, this if statement allows #ahah[wrapper] to be optional.
if (new_content.parents('html').length > 0) {
// Apply any settings from the returned JSON if available.
var settings = response.settings || Drupal.settings;
Drupal.attachBehaviors(new_content, settings);
}
};
/**
* Handler for the form redirection error.
*/
Drupal.ahah.prototype.error = function (response, uri) {
alert(Drupal.ahahError(response, uri));
// Resore the previous action and target to the form.
$(this.element).parent('form').attr({ action: this.form_action, target: this.form_target });
// Remove the progress element.
if (this.progress.element) {
$(this.progress.element).remove();
}
if (this.progress.object) {
this.progress.object.stopMonitoring();
}
// Undo hide.
$(this.wrapper).show();
// Re-enable the element.
$(this.element).removeClass('progress-disabled').attr('disabled', false);
};
})(jQuery);
// $Id$
(function ($) {
/**
* Provides AJAX page updating via jQuery $.ajax (Asynchronous JavaScript and XML).
*
* AJAX is a method of making a request via Javascript while viewing an HTML
* page. The request returns an array of commands encoded in JSON, which is
* then executed to make any changes that are necessary to the page.
*
* Drupal uses this file to enhance form elements with #ajax['path'] and
* #ajax['wrapper'] properties. If set, this file will automatically be included
* to provide AJAX capabilities.
*/
Drupal.ajax = Drupal.ajax || {};
/**
* Attaches the AJAX behavior to each AJAX form element.
*/
Drupal.behaviors.AJAX = {
attach: function (context, settings) {
// Load all AJAX behaviors specified in the settings.
for (var base in settings.ajax) {
if (!$('#' + base + '.ajax-processed').length) {
var element_settings = settings.ajax[base];
$(element_settings.selector).each(function () {
Drupal.ajax[base] = new Drupal.ajax(base, this, element_settings);
});
$('#' + base).addClass('ajax-processed');
}
}
// Bind AJAX behaviors to all items showing the class.
$('.use-ajax:not(.ajax-processed)').addClass('ajax-processed').each(function () {
var element_settings = {};
// For anchor tags, these will go to the target of the anchor rather
// than the usual location.
if ($(this).attr('href')) {
element_settings.url = $(this).attr('href');
}
var base = $(this).attr('id');
Drupal.ajax[base] = new Drupal.ajax(base, this, element_settings);
});
// This class means to submit the form to the action using AJAX.
$('.use-ajax-submit:not(.ajax-processed)').addClass('ajax-processed').each(function () {
var element_settings = {};
// AJAX submits specified in this manner automatically submit to the
// normal form action.
element_settings.url = $(this.form).attr('action');
element_settings.set_click = TRUE;
var base = $(this).attr('id');
Drupal.ajax[base] = new Drupal.ajax(base, this, element_settings);
});
}
};
/**
* AJAX object.
*
* All AJAX objects on a page are accessible through the global Drupal.ajax
* object and are keyed by the submit button's ID. You can access them from
* your module's JavaScript file to override properties or functions.
*
* For example, if your AJAX enabled button has the ID 'edit-submit', you can
* redefine the function that is called to insert the new content like this
* (inside a Drupal.behaviors attach block):
* @code
* Drupal.behaviors.myCustomAJAXStuff = {
* attach: function (context, settings) {
* Drupal.ajax['edit-submit'].commands.insert = function (ajax, response, status) {
* new_content = $(response.data);
* $('#my-wrapper').append(new_content);
* alert('New content was appended to #my-wrapper');
* }
* }
* };
* @endcode
*/
Drupal.ajax = function (base, element, element_settings) {
var defaults = {
url: 'system/ajax',
event: 'mousedown',
keypress: true,
selector: '#' + base,
effect: 'none',
speed: 'slow',
method: 'replace',
progress: {
type: 'bar',
message: 'Please wait...'
},
button: {},
};
$.extend(this, defaults, element_settings);
this.element = element;
// Replacing 'nojs' with 'ajax' in the URL allows for an easy method to let
// the server detect when it needs to degrade gracefully.
this.url = element_settings.url.replace('/nojs/', '/ajax/');
this.wrapper = '#' + element_settings.wrapper;
// If there isn't a form, jQuery.ajax() will be used instead, allowing us to
// bind AJAX to links as well.
if (this.element.form) {
this.form = $(this.element.form);
}
// Set the options for the ajaxSubmit function.
// The 'this' variable will not persist inside of the options object.
var ajax = this;
var options = {
url: ajax.url,
data: ajax.button,
beforeSubmit: function (form_values, element_settings, options) {
return ajax.beforeSubmit(form_values, element_settings, options);
},
success: function (response, status) {
// Sanity check for browser support (object expected).
// When using iFrame uploads, responses must be returned as a string.
if (typeof response == 'string') {
response = Drupal.parseJson(response);
}
return ajax.success(response, status);
},
complete: function (response, status) {
if (status == 'error' || status == 'parsererror') {
return ajax.error(response, ajax.url);
}
},
dataType: 'json',
type: 'POST'
};
// Bind the ajaxSubmit function to the element event.
$(this.element).bind(element_settings.event, function () {
if (ajax.form) {
// If setClick is set, we must set this to ensure that the button's
// value is passed.
if (ajax.setClick) {
// Mark the clicked button. 'form.clk' is a special variable for
// ajaxSubmit that tells the system which element got clicked to
// trigger the submit. Without it there would be no 'op' or
// equivalent.
ajax.form.clk = this.element;
}
ajax.form.ajaxSubmit(options);
}
else {
$.ajax(options);
}
return false;
});
// If necessary, enable keyboard submission so that AJAX behaviors
// can be triggered through keyboard input as well as e.g. a mousedown
// action.
if (element_settings.keypress) {
$(element_settings.element).keypress(function (event) {
// Detect enter key.
if (event.keyCode == 13) {
$(element_settings.element).trigger(element_settings.event);