Newer
Older
Angie Byron
committed
// $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 () {
Angie Byron
committed
element_settings.element = this;
Angie Byron
committed
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');
Angie Byron
committed
element_settings.event = 'click';
Angie Byron
committed
}
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');
// Form submit button clicks need to tell the form what was clicked so
// it gets passed in the POST request.
element_settings.setClick = true;
// Form buttons use the 'click' event rather than mousedown.
element_settings.event = 'click';
// Clicked form buttons look better with the throbber than the progress bar.
element_settings.progress = { 'type': 'throbber' };
Angie Byron
committed
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
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',
Dries Buytaert
committed
method: 'replaceWith',
Angie Byron
committed
progress: {
type: 'bar',
message: 'Please wait...'
},
submit: {
'js': true
}
Angie Byron
committed
};
$.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.
Dries Buytaert
committed
// There are five scenarios to check for:
// 1. /nojs/
// 2. /nojs$ - The end of a URL string.
// 3. /nojs? - Followed by a query (with clean URLs enabled).
// E.g.: path/nojs?destination=foobar
// 4. /nojs& - Followed by a query (without clean URLs enabled).
// E.g.: ?q=path/nojs&destination=foobar
// 5. /nojs# - Followed by a fragment.
// E.g.: path/nojs#myfragment
this.url = element_settings.url.replace(/\/nojs(\/|$|\?|&|#)/g, '/ajax$1');
Angie Byron
committed
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;
ajax.options = {
Angie Byron
committed
url: ajax.url,
data: ajax.submit,
beforeSerialize: function (element_settings, options) {
return ajax.beforeSerialize(element_settings, options);
},
Angie Byron
committed
beforeSubmit: function (form_values, element_settings, options) {
ajax.ajaxing = true;
Angie Byron
committed
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 = $.parseJSON(response);
Angie Byron
committed
}
return ajax.success(response, status);
},
complete: function (response, status) {
ajax.ajaxing = false;
Angie Byron
committed
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.ajaxing) {
return false;
}
Angie Byron
committed
try {
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.
this.form.clk = this;
}
ajax.form.ajaxSubmit(ajax.options);
}
else {
Dries Buytaert
committed
ajax.beforeSerialize(ajax.element, ajax.options);
$.ajax(ajax.options);
Angie Byron
committed
}
catch (e) {
alert("An error occurred while attempting to process " + ajax.options.url + ": " + e.message);
Angie Byron
committed
}
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 and space bar.
if (event.which == 13 || event.which == 32) {
Angie Byron
committed
$(element_settings.element).trigger(element_settings.event);
return false;
}
});
}
};
/**
* Handler for the form serialization.
*
* Runs before the beforeSubmit() handler (see below), and unlike that one, runs
* before field data is collected.
*/
Drupal.ajax.prototype.beforeSerialize = function (element, options) {
// Allow detaching behaviors to update field values before collecting them.
Dries Buytaert
committed
// This is only needed when field values are added to the POST data, so only
// when there is a form such that this.form.ajaxSubmit() is used instead of
// $.ajax(). When there is no form and $.ajax() is used, beforeSerialize()
// isn't called, but don't rely on that: explicitly check this.form.
if (this.form) {
var settings = this.settings || Drupal.settings;
Drupal.detachBehaviors(this.form, settings, 'serialize');
}
Angie Byron
committed
Dries Buytaert
committed
// Prevent duplicate HTML ids in the returned markup.
// @see drupal_html_id()
Dries Buytaert
committed
options.data['ajax_html_ids[]'] = [];
Dries Buytaert
committed
$('[id]').each(function () {
Dries Buytaert
committed
options.data['ajax_html_ids[]'].push(this.id);
Dries Buytaert
committed
});
Dries Buytaert
committed
// Allow Drupal to return new JavaScript and CSS files to load without
// returning the ones already loaded.
Dries Buytaert
committed
// @see ajax_base_page_theme()
// @see drupal_get_css()
// @see drupal_get_js()
options.data['ajax_page_state[theme]'] = Drupal.settings.ajaxPageState.theme;
options.data['ajax_page_state[theme_token]'] = Drupal.settings.ajaxPageState.theme_token;
Dries Buytaert
committed
for (var key in Drupal.settings.ajaxPageState.css) {
Dries Buytaert
committed
options.data['ajax_page_state[css][' + key + ']'] = 1;
Dries Buytaert
committed
}
for (var key in Drupal.settings.ajaxPageState.js) {
Dries Buytaert
committed
options.data['ajax_page_state[js][' + key + ']'] = 1;
Dries Buytaert
committed
}
Dries Buytaert
committed
};
/**
* Handler for the form redirection submission.
*/
Drupal.ajax.prototype.beforeSubmit = function (form_values, element, options) {
// Disable the element that received the change.
$(this.element).addClass('progress-disabled').attr('disabled', true);
Dries Buytaert
committed
Angie Byron
committed
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
// Insert progressbar or throbber.
if (this.progress.type == 'bar') {
var progressBar = new Drupal.progressBar('ajax-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('ajax-progress ajax-progress-bar');
this.progress.object = progressBar;
$(this.element).after(this.progress.element);
}
else if (this.progress.type == 'throbber') {
this.progress.element = $('<div class="ajax-progress ajax-progress-throbber"><div class="throbber"> </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.ajax.prototype.success = function (response, status) {
// Remove the progress element.
if (this.progress.element) {
$(this.progress.element).remove();
}
if (this.progress.object) {
this.progress.object.stopMonitoring();
}
Dries Buytaert
committed
$(this.element).removeClass('progress-disabled').removeAttr('disabled');
Angie Byron
committed
Drupal.freezeHeight();
Dries Buytaert
committed
for (var i in response) {
Angie Byron
committed
if (response[i]['command'] && this.commands[response[i]['command']]) {
this.commands[response[i]['command']](this, response[i], status);
}
}
Dries Buytaert
committed
// Reattach behaviors, if they were detached in beforeSerialize(). The
// attachBehaviors() called on the new content from processing the response
// commands is not sufficient, because behaviors from the entire form need
// to be reattached.
Dries Buytaert
committed
if (this.form) {
var settings = this.settings || Drupal.settings;
Drupal.attachBehaviors(this.form, settings);
}
Angie Byron
committed
Drupal.unfreezeHeight();
// Remove any response-specific settings so they don't get used on the next
// call by mistake.
Dries Buytaert
committed
this.settings = null;
Angie Byron
committed
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
};
/**
* Build an effect object which tells us how to apply the effect when adding new HTML.
*/
Drupal.ajax.prototype.getEffect = function (response) {
var type = response.effect || this.effect;
var speed = response.speed || this.speed;
var effect = {};
if (type == 'none') {
effect.showEffect = 'show';
effect.hideEffect = 'hide';
effect.showSpeed = '';
}
else if (type == 'fade') {
effect.showEffect = 'fadeIn';
effect.hideEffect = 'fadeOut';
effect.showSpeed = speed;
}
else {
effect.showEffect = type + 'Toggle';
effect.hideEffect = type + 'Toggle';
effect.showSpeed = speed;
}
return effect;
Angie Byron
committed
/**
* Handler for the form redirection error.
*/
Drupal.ajax.prototype.error = function (response, uri) {
alert(Drupal.ajaxError(response, uri));
// 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.
Dries Buytaert
committed
$(this.element).removeClass('progress-disabled').removeAttr('disabled');
Dries Buytaert
committed
// Reattach behaviors, if they were detached in beforeSerialize().
if (this.form) {
var settings = response.settings || this.settings || Drupal.settings;
Drupal.attachBehaviors(this.form, settings);
}
Angie Byron
committed
};
/**
* Provide a series of commands that the server can request the client perform.
*/
Drupal.ajax.prototype.commands = {
/**
* Command to insert new content into the DOM.
*/
insert: function (ajax, response, status) {
// Get information from the response. If it is not there, default to
// our presets.
var wrapper = response.selector ? $(response.selector) : $(ajax.wrapper);
var method = response.method || ajax.method;
var effect = ajax.getEffect(response);
// We don't know what response.data contains: it might be a string of text
// without HTML, so don't rely on jQuery correctly iterpreting
// $(response.data) as new HTML rather than a CSS selector. Also, if
// response.data contains top-level text nodes, they get lost with either
// $(response.data) or $('<div></div>').replaceWith(response.data).
var new_content_wrapped = $('<div></div>').html(response.data);
var new_content = new_content_wrapped.contents();
// For legacy reasons, the effects processing code assumes that new_content
// consists of a single top-level element. Also, it has not been
// sufficiently tested whether attachBehaviors() can be successfully called
// with a context object that includes top-level text nodes. However, to
// give developers full control of the HTML appearing in the page, and to
// enable AJAX content to be inserted in places where DIV elements are not
// allowed (e.g., within TABLE, TR, and SPAN parents), we check if the new
// content satisfies the requirement of a single top-level element, and
// only use the container DIV created above when it doesn't. For more
// information, please see http://drupal.org/node/736066.
if (new_content.length != 1 || new_content.get(0).nodeType != 1) {
new_content = new_content_wrapped;
}
Angie Byron
committed
// If removing content from the wrapper, detach behaviors first.
switch (method) {
case 'html':
case 'replaceWith':
case 'replaceAll':
case 'empty':
case 'remove':
var settings = response.settings || ajax.settings || Drupal.settings;
Drupal.detachBehaviors(wrapper, settings);
}
Angie Byron
committed
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
// Add the new content to the page.
wrapper[method](new_content);
// Immediately hide the new content if we're using any effects.
if (effect.showEffect != 'show') {
new_content.hide();
}
// Determine which effect to use and what content will receive the
// effect, then show the new content.
if ($('.ajax-new-content', new_content).length > 0) {
$('.ajax-new-content', new_content).hide();
new_content.show();
$('.ajax-new-content', new_content)[effect.showEffect](effect.showSpeed);
}
else if (effect.showEffect != 'show') {
new_content[effect.showEffect](effect.showSpeed);
}
// Attach all JavaScript behaviors to the new content, if it was successfully
// added to the page, this if statement allows #ajax['wrapper'] to be
// optional.
if (new_content.parents('html').length > 0) {
// Apply any settings from the returned JSON if available.
var settings = response.settings || ajax.settings || Drupal.settings;
Drupal.attachBehaviors(new_content, settings);
}
},
/**
* Command to remove a chunk from the page.
*/
remove: function (ajax, response, status) {
var settings = response.settings || ajax.settings || Drupal.settings;
Drupal.detachBehaviors($(response.selector), settings);
Angie Byron
committed
$(response.selector).remove();
},
/**
* Command to mark a chunk changed.
*/
changed: function (ajax, response, status) {
if (!$(response.selector).hasClass('ajax-changed')) {
$(response.selector).addClass('ajax-changed');
if (response.asterisk) {
$(response.selector).find(response.asterisk).append(' <span class="ajax-changed">*</span> ');
}
}
},
/**
* Command to provide an alert.
*/
alert: function (ajax, response, status) {
alert(response.text, response.title);
},
/**
* Command to provide the jQuery css() function.
*/
css: function (ajax, response, status) {
$(response.selector).css(response.argument);
},
Angie Byron
committed
/**
* Command to set the settings that will be used for other commands in this response.
*/
settings: function (ajax, response, status) {
Angie Byron
committed
if (response.merge) {
$.extend(true, Drupal.settings, response.settings);
}
else {
ajax.settings = response.settings;
}
Angie Byron
committed
},
/**
* Command to attach data using jQuery's data API.
*/
data: function (ajax, response, status) {
$(response.selector).data(response.name, response.value);
},
/**
* Command to restripe a table.
*/
restripe: function (ajax, response, status) {
// :even and :odd are reversed because jQuery counts from 0 and
// we count from 1, so we're out of sync.
// Match immediate children of the parent element to allow nesting.
$('> tbody > tr:visible, > tr:visible', $(response.selector))
.removeClass('odd even')
.filter(':even').addClass('odd').end()
.filter(':odd').addClass('even');
Angie Byron
committed
}
};
})(jQuery);