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
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;
var options = {
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.
ajax.form.clk = this.element;
}
ajax.form.ajaxSubmit(options);
}
else {
$.ajax(options);
}
Angie Byron
committed
}
catch (e) {
alert("An error occurred while attempting to process " + 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.
if (event.keyCode == 13) {
$(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
/**
* 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
// Prevent duplicate HTML ids in the returned markup.
// @see drupal_html_id()
$('[id]').each(function () {
form_values.push({ name: 'ajax_html_ids[]', value: this.id });
});
Angie Byron
committed
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
// 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();
}
$(this.element).removeClass('progress-disabled').attr('disabled', false);
Drupal.freezeHeight();
for (i in response) {
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
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
Drupal.unfreezeHeight();
// Remove any response-specific settings so they don't get used on the next
// call by mistake.
this.settings = {};
};
/**
* 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.
$(this.element).removeClass('progress-disabled').attr('disabled', false);
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);
// Manually insert HTML into the jQuery object, using $() directly crashes
// Safari with long string lengths. http://dev.jquery.com/ticket/3178
var new_content = $('<div></div>').html(response.data);
// 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
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
// 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);