Commit 4c49e1e5 authored by alexpott's avatar alexpott

Issue #1533366 by nod_, valthebald, Jelle_S, Wim Leers: Simplify and optimize...

Issue #1533366 by nod_, valthebald, Jelle_S, Wim Leers: Simplify and optimize Drupal.ajax() instantiation and implementation
parent 7ec2ab57
......@@ -2,20 +2,6 @@
"use strict";
/**
* 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['url'] 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.
*/
......@@ -29,7 +15,8 @@
}
$(element_settings.selector).once('drupal-ajax').each(function () {
element_settings.element = this;
Drupal.ajax[element_settings.selector] = new Drupal.ajax(base, this, element_settings);
element_settings.base = base;
Drupal.ajax(element_settings);
});
}
......@@ -54,8 +41,9 @@
}
element_settings.dialogType = $(this).data('dialog-type');
element_settings.dialog = $(this).data('dialog-options');
var baseUseAjax = $(this).attr('id');
Drupal.ajax[baseUseAjax] = new Drupal.ajax(baseUseAjax, this, element_settings);
element_settings.base = $(this).attr('id');
element_settings.element = this;
Drupal.ajax(element_settings);
});
// This class means to submit the form to the action using Ajax.
......@@ -72,15 +60,16 @@
element_settings.event = 'click';
// Clicked form buttons look better with the throbber than the progress bar.
element_settings.progress = {'type': 'throbber'};
element_settings.base = $(this).attr('id');
element_settings.element = this;
var baseUseAjaxSubmit = $(this).attr('id');
Drupal.ajax[baseUseAjaxSubmit] = new Drupal.ajax(baseUseAjaxSubmit, this, element_settings);
Drupal.ajax(element_settings);
});
}
};
/**
* Extends Error to provide handling for Errors in AJAX
* Extends Error to provide handling for Errors in Ajax.
*/
Drupal.AjaxError = function (xmlhttp, uri) {
......@@ -115,7 +104,7 @@
responseText = "\n" + Drupal.t("ResponseText: !responseText", {'!responseText': $.trim(xmlhttp.responseText)});
}
catch (e) {
// empty
// Empty.
}
// Make the responseText more readable by stripping HTML tags and newlines.
......@@ -133,32 +122,114 @@
Drupal.AjaxError.prototype.constructor = Drupal.AjaxError;
/**
* Ajax object.
* Provides Ajax page updating via jQuery $.ajax.
*
* This function is designed to improve developer experience by wrapping the
* initialization of Drupal.Ajax objects and storing all created object in the
* Drupal.ajax.instances array.
*
* @example
* Drupal.behaviors.myCustomAJAXStuff = {
* attach: function (context, settings) {
*
* var ajaxSettings = {
* url: 'my/url/path',
* // If the old version of Drupal.ajax() needs to be used those
* // properties can be added
* base: 'myBase',
* element: $(context).find('.someElement')
* };
*
* var myAjaxObject = Drupal.ajax(ajaxSettings);
*
* // Declare a new Ajax command specifically for this Ajax object.
* myAjaxObject.commands.insert = function (ajax, response, status) {
* $('#my-wrapper').append(response.data);
* alert('New content was appended to #my-wrapper');
* };
*
* // This command will remove this Ajax object from the page.
* myAjaxObject.commands.destroyObject = function (ajax, response, status) {
* Drupal.ajax.instances[this.instanceIndex] = null;
* };
*
* // Programmatically trigger the Ajax request.
* myAjaxObject.execute();
* }
* };
*
* @see Drupal.AjaxCommands
*
* @param {object} settings
* The settings object passed to Drupal.Ajax constructor.
* @param {string} [settings.base]
* Base is passed to Drupal.Ajax constructor as the 'base' parameter.
* @param {HTMLElement} [settings.element]
* Element parameter of Drupal.Ajax constructor, element on which
* event listeners will be bound.
*
* @return {Drupal.Ajax}
*/
Drupal.ajax = function (settings) {
if (arguments.length !== 1) {
throw new Error('Drupal.ajax() function must be called with one configuration object only');
}
// Map those config keys to variables for the old Drupal.ajax function.
var base = settings.base || false;
var element = settings.element || false;
delete settings.base;
delete settings.element;
// By default do not display progress for ajax calls without an element.
if (!settings.progress && !element) {
settings.progress = false;
}
var ajax = new Drupal.Ajax(base, element, settings);
ajax.instanceIndex = Drupal.ajax.instances.length;
Drupal.ajax.instances.push(ajax);
return ajax;
};
/**
* Contains all created Ajax objects.
*
* @type {Array}
*/
Drupal.ajax.instances = [];
/**
* Ajax constructor.
*
* The Ajax 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['url'] and
* #ajax['wrapper'] properties. If set, this file will automatically be
* included to provide Ajax capabilities.
*
* 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.
* @constructor
*
* 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
* @param {string} [base]
* Base parameter of Drupal.Ajax constructor
* @param {HTMLElement} [element]
* Element parameter of Drupal.Ajax constructor, element on which
* event listeners will be bound.
* @param {object} element_settings
* @param {string} element_settings.url
* Target of the Ajax request.
* @param {string} [element_settings.event]
* Event bound to settings.element which will trigger the Ajax request.
* @param {string} [element_settings.method]
* Name of the jQuery method used to insert new content in the targeted
* element.
*/
Drupal.ajax = function (base, element, element_settings) {
Drupal.Ajax = function (base, element, element_settings) {
var defaults = {
event: 'mousedown',
event: element ? 'mousedown' : null,
keypress: true,
selector: '#' + base,
selector: base ? '#' + base : null,
effect: 'none',
speed: 'none',
method: 'replaceWith',
......@@ -174,6 +245,7 @@
$.extend(this, defaults, element_settings);
this.commands = new Drupal.AjaxCommands();
this.instanceIndex = false;
// @todo Remove this after refactoring the PHP code to:
// - Call this 'selector'.
......@@ -188,24 +260,25 @@
// 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) {
if (this.element && this.element.form) {
this.$form = $(this.element.form);
}
// If no Ajax callback URL was given, use the link href or form action.
if (!this.url) {
if ($(element).is('a')) {
this.url = $(element).attr('href');
var $element = $(this.element);
if ($element.is('a')) {
this.url = $element.attr('href');
}
else if (element.form) {
else if (this.element && element.form) {
this.url = this.$form.attr('action');
// @todo If there's a file input on this form, then jQuery will submit the
// AJAX response with a hidden Iframe rather than the XHR object. If the
// Ajax response with a hidden Iframe rather than the XHR object. If the
// response to the submission is an HTTP redirect, then the Iframe will
// follow it, but the server won't content negotiate it correctly,
// because there won't be an ajax_iframe_upload POST variable. Until we
// figure out a work around to this problem, we prevent AJAX-enabling
// figure out a work around to this problem, we prevent Ajax-enabling
// elements that submit to the same URL as the form when there's a file
// input. For example, this means the Delete button on the edit form of
// an Article node doesn't open its confirmation form in a dialog.
......@@ -289,7 +362,7 @@
// If necessary, prevent the browser default action of an additional event.
// For example, prevent the browser default action of a click, even if the
// AJAX behavior binds to mousedown.
// Ajax behavior binds to mousedown.
if (element_settings.prevent) {
$(ajax.element).on(element_settings.prevent, false);
}
......@@ -303,6 +376,30 @@
*/
Drupal.ajax.WRAPPER_FORMAT = '_wrapper_format';
/**
* Execute the ajax request.
*
* Allows developers to execute an Ajax request manually without specifying
* an event to respond to.
*/
Drupal.Ajax.prototype.execute = function () {
// Do not perform another ajax command if one is already in progress.
if (this.ajaxing) {
return;
}
try {
this.beforeSerialize(this.element, this.options);
$.ajax(this.options);
}
catch (e) {
// Unset the ajax.ajaxing flag here because it won't be unset during
// the complete response.
this.ajaxing = false;
window.alert("An error occurred while attempting to process " + this.options.url + ": " + e.message);
}
};
/**
* Handle a key press.
*
......@@ -313,7 +410,7 @@
* and 32. RETURN is often used to submit a form when in a textfield, and
* SPACE is often used to activate an element without submitting.
*/
Drupal.ajax.prototype.keypressResponse = function (element, event) {
Drupal.Ajax.prototype.keypressResponse = function (element, event) {
// Create a synonym for this to reduce code confusion.
var ajax = this;
......@@ -336,16 +433,16 @@
* When an event that triggers an Ajax response happens, this method will
* perform the actual Ajax call. It is bound to the event using
* bind() in the constructor, and it uses the options specified on the
* ajax object.
* Ajax object.
*/
Drupal.ajax.prototype.eventResponse = function (element, event) {
Drupal.Ajax.prototype.eventResponse = function (element, event) {
event.preventDefault();
event.stopPropagation();
// Create a synonym for this to reduce code confusion.
var ajax = this;
// Do not perform another ajax command if one is already in progress.
// Do not perform another Ajax command if one is already in progress.
if (ajax.ajaxing) {
return;
}
......@@ -383,7 +480,7 @@
* Runs before the beforeSend() handler (see below), and unlike that one, runs
* before field data is collected.
*/
Drupal.ajax.prototype.beforeSerialize = function (element, options) {
Drupal.Ajax.prototype.beforeSerialize = function (element, options) {
// Allow detaching behaviors to update field values before collecting them.
// 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
......@@ -419,7 +516,7 @@
/**
* Modify form values prior to form submission.
*/
Drupal.ajax.prototype.beforeSubmit = function (form_values, element, options) {
Drupal.Ajax.prototype.beforeSubmit = function (form_values, element, options) {
// This function is left empty to make it simple to override for modules
// that wish to add functionality here.
};
......@@ -427,7 +524,7 @@
/**
* Prepare the Ajax request before it is sent.
*/
Drupal.ajax.prototype.beforeSend = function (xmlhttprequest, options) {
Drupal.Ajax.prototype.beforeSend = function (xmlhttprequest, options) {
// For forms without file inputs, the jQuery Form plugin serializes the form
// values, and then calls jQuery's $.ajax() function, which invokes this
// handler. In this circumstance, options.extraData is never used. For forms
......@@ -462,36 +559,57 @@
// from changing its value.
$(this.element).prop('disabled', true);
// Insert progressbar or throbber.
if (this.progress.type === 'bar') {
var progressBar = new Drupal.ProgressBar('ajax-progress-' + this.element.id, $.noop, this.progress.method, $.noop);
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);
if (!this.progress || !this.progress.type) {
return;
}
else if (this.progress.type === 'throbber') {
this.progress.element = $('<div class="ajax-progress ajax-progress-throbber"><div class="throbber">&nbsp;</div></div>');
if (this.progress.message) {
this.progress.element.find('.throbber').after('<div class="message">' + this.progress.message + '</div>');
}
// Insert progress indicator
var progressIndicatorMethod = 'setProgressIndicator' + this.progress.type.slice(0, 1).toUpperCase() + this.progress.type.slice(1).toLowerCase();
if (progressIndicatorMethod in this && typeof this[progressIndicatorMethod] === 'function') {
this[progressIndicatorMethod].call(this);
$(this.element).after(this.progress.element);
}
else if (this.progress.type === 'fullscreen') {
this.progress.element = $('<div class="ajax-progress ajax-progress-fullscreen">&nbsp;</div>');
$('body').after(this.progress.element);
};
/**
* Sets the progress bar progress indicator.
*/
Drupal.Ajax.prototype.setProgressIndicatorBar = function () {
var progressBar = new Drupal.ProgressBar('ajax-progress-' + this.element.id, $.noop, this.progress.method, $.noop);
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);
};
/**
* Sets the throbber progress indicator.
*/
Drupal.Ajax.prototype.setProgressIndicatorThrobber = function () {
this.progress.element = $('<div class="ajax-progress ajax-progress-throbber"><div class="throbber">&nbsp;</div></div>');
if (this.progress.message) {
this.progress.element.find('.throbber').after('<div class="message">' + this.progress.message + '</div>');
}
$(this.element).after(this.progress.element);
};
/**
* Sets the fullscreen progress indicator.
*/
Drupal.Ajax.prototype.setProgressIndicatorFullscreen = function () {
this.progress.element = $('<div class="ajax-progress ajax-progress-fullscreen">&nbsp;</div>');
$('body').after(this.progress.element);
};
/**
* Handler for the form redirection completion.
*/
Drupal.ajax.prototype.success = function (response, status) {
Drupal.Ajax.prototype.success = function (response, status) {
// Remove the progress element.
if (this.progress.element) {
$(this.progress.element).remove();
......@@ -524,7 +642,7 @@
/**
* Build an effect object which tells us how to apply the effect when adding new HTML.
*/
Drupal.ajax.prototype.getEffect = function (response) {
Drupal.Ajax.prototype.getEffect = function (response) {
var type = response.effect || this.effect;
var speed = response.speed || this.speed;
......@@ -551,7 +669,7 @@
/**
* Handler for the form redirection error.
*/
Drupal.ajax.prototype.error = function (response, uri) {
Drupal.Ajax.prototype.error = function (response, uri) {
// Remove the progress element.
if (this.progress.element) {
$(this.progress.element).remove();
......
......@@ -158,23 +158,20 @@
// Add a "Loading…" message, hide it underneath the CKEditor toolbar, create
// a Drupal.ajax instance to load the dialog and trigger it.
var $content = $('<div class="ckeditor-dialog-loading"><span style="top: -40px;" class="ckeditor-dialog-loading-link"><a>' + Drupal.t('Loading...') + '</a></span></div>');
var $content = $('<div class="ckeditor-dialog-loading"><span style="top: -40px;" class="ckeditor-dialog-loading-link">' + Drupal.t('Loading...') + '</span></div>');
$content.appendTo($target);
new Drupal.ajax('ckeditor-dialog', $content.find('a').get(0), {
var ckeditorAjaxDialog = Drupal.ajax({
dialog: dialogSettings,
dialogType: 'modal',
selector: '.ckeditor-dialog-loading-link',
url: url,
event: 'ckeditor-internal.ckeditor',
progress: {'type': 'throbber'},
submit: {
editor_object: existingValues
}
});
$content.find('a')
.on('click', function () { return false; })
.trigger('ckeditor-internal.ckeditor');
ckeditorAjaxDialog.execute();
// After a short delay, show "Loading…" message.
window.setTimeout(function () {
......
......@@ -171,11 +171,9 @@
var fieldID = this.fieldModel.get('fieldID');
// Create a Drupal.ajax instance to load the form.
var textLoaderAjax = new Drupal.ajax(fieldID, this.$el, {
var textLoaderAjax = Drupal.ajax({
url: Drupal.quickedit.util.buildUrl(fieldID, Drupal.url('editor/!entity_type/!id/!field_name/!langcode/!view_mode')),
event: 'editor-internal.editor',
submit: {nocssjs: true},
progress: {type: null} // No progress indicator.
submit: {nocssjs: true}
});
// Implement a scoped editorGetUntransformedText AJAX command: calls the
......@@ -186,7 +184,7 @@
// This will ensure our scoped editorGetUntransformedText AJAX command
// gets called.
this.$el.trigger('editor-internal.editor');
textLoaderAjax.execute();
}
});
......
......@@ -362,18 +362,10 @@
save: function (options) {
var entityModel = this;
// @todo Simplify this once https://drupal.org/node/1533366 lands.
// @see https://drupal.org/node/2029999.
var id = 'quickedit-save-entity';
// Create a temporary element to be able to use Drupal.ajax.
var $el = $('#quickedit-entity-toolbar').find('.action-save'); // This is the span element inside the button.
// Create a Drupal.ajax instance to save the entity.
var entitySaverAjax = new Drupal.ajax(id, $el, {
var entitySaverAjax = Drupal.ajax({
url: Drupal.url('quickedit/entity/' + entityModel.get('entityID')),
event: 'quickedit-save.quickedit',
progress: {type: 'none'},
error: function () {
$el.off('quickedit-save.quickedit');
// Let the Drupal.quickedit.EntityModel Backbone model's error()=
// method handle errors.
options.error.call(entityModel);
......@@ -381,8 +373,6 @@
});
// Entity saved successfully.
entitySaverAjax.commands.quickeditEntitySaved = function (ajax, response, status) {
// Clean up.
$(ajax.element).off('quickedit-save.quickedit');
// All fields have been moved from PrivateTempStore to permanent
// storage, update the "inTempStore" attribute on FieldModels, on the
// EntityModel and clear EntityModel's "fieldInTempStore" attribute.
......@@ -399,7 +389,7 @@
};
// Trigger the AJAX request, which will will return the
// quickeditEntitySaved AJAX command to which we then react.
$el.trigger('quickedit-save.quickedit');
entitySaverAjax.execute();
},
/**
......
......@@ -408,18 +408,11 @@
return;
}
// @todo Simplify this once https://drupal.org/node/1533366 lands.
// @see https://drupal.org/node/2029999.
var id = 'quickedit-load-editors';
// Create a temporary element to be able to use Drupal.ajax.
var $el = $('<div id="' + id + '" class="hidden"></div>').appendTo('body');
// Create a Drupal.ajax instance to load the form.
var loadEditorsAjax = new Drupal.ajax(id, $el, {
// Create a Drupal.Ajax instance to load the form.
var loadEditorsAjax = Drupal.ajax({
url: Drupal.url('quickedit/attachments'),
event: 'quickedit-internal.quickedit',
submit: {'editors[]': missingEditors},
// No progress indicator.
progress: {type: null}
submit: {'editors[]': missingEditors}
});
// Implement a scoped insert AJAX command: calls the callback after all AJAX
// command functions have been executed (hence the deferred calling).
......@@ -427,12 +420,10 @@
loadEditorsAjax.commands.insert = function (ajax, response, status) {
_.defer(callback);
realInsert(ajax, response, status);
$el.off('quickedit-internal.quickedit');
$el.remove();
};
// Trigger the AJAX request, which will should return AJAX commands to insert
// any missing attachments.
$el.trigger('quickedit-internal.quickedit');
loadEditorsAjax.execute();
}
/**
......
......@@ -89,21 +89,16 @@
* commands.
*/
load: function (options, callback) {
var $el = options.$el;
var fieldID = options.fieldID;
// Create a Drupal.ajax instance to load the form.
var formLoaderAjax = new Drupal.ajax(fieldID, $el, {
var formLoaderAjax = Drupal.ajax({
url: Drupal.quickedit.util.buildUrl(fieldID, Drupal.url('quickedit/form/!entity_type/!id/!field_name/!langcode/!view_mode')),
event: 'quickedit-internal.quickedit',
submit: {
nocssjs: options.nocssjs,
reset: options.reset
},
progress: {type: null}, // No progress indicator.
error: function (xhr, url) {
$el.off('quickedit-internal.quickedit');
// Show a modal to inform the user of the network error.
var fieldLabel = Drupal.quickedit.metadata.get(fieldID, 'label');
var message = Drupal.t('Could not load the form for <q>@field-label</q>, either due to a website problem or a network connection problem.<br>Please try again.', {'@field-label': fieldLabel});
......@@ -118,11 +113,10 @@
// Implement a scoped quickeditFieldForm AJAX command: calls the callback.
formLoaderAjax.commands.quickeditFieldForm = function (ajax, response, status) {
callback(response.data, ajax);
$el.off('quickedit-internal.quickedit');
formLoaderAjax = null;
Drupal.ajax.instances[this.instanceIndex] = null;
};
// This will ensure our scoped quickeditFieldForm AJAX command gets called.
$el.trigger('quickedit-internal.quickedit');
formLoaderAjax.execute();
},
/**
......@@ -143,7 +137,7 @@
url: $submit.closest('form').attr('action'),
setClick: true,
event: 'click.quickedit',
progress: {type: null},
progress: false,
submit: {
nocssjs: options.nocssjs,
other_view_modes: options.other_view_modes
......@@ -156,10 +150,12 @@
this.commands[response[i].command](this, response[i], status);
}
}
}
},
base: $submit.attr('id'),
element: $submit[0]
};
return new Drupal.ajax($submit.attr('id'), $submit[0], settings);
return Drupal.ajax(settings);
},
/**
......
......@@ -78,16 +78,23 @@
// @code
// jQuery('.view-name').trigger('RefreshView');
// @endcode
var self_settings = this.element_settings;
self_settings.event = 'RefreshView';
this.refreshViewAjax = new Drupal.ajax(this.selector, this.$view, self_settings);
var self_settings = $.extend({}, this.element_settings, {
event: 'RefreshView',
base: this.selector,
element: this.$view
});
this.refreshViewAjax = Drupal.ajax(self_settings);
};
Drupal.views.ajaxView.prototype.attachExposedFormAjax = function () {
var button = $('input[type=submit], input[type=image]', this.$exposed_form);
button = button[0];
this.exposedFormAjax = new Drupal.ajax($(button).attr('id'), button, this.element_settings);
var self_settings = $.extend({}, this.element_settings, {
base: $(button).attr('id'),
element: button
});
this.exposedFormAjax = Drupal.ajax(self_settings);
};
Drupal.views.ajaxView.prototype.filterNestedViews = function () {
......@@ -121,8 +128,12 @@
Drupal.Views.parseViewArgs(href, this.settings.view_base_path)
);
this.element_settings.submit = viewData;
this.pagerAjax = new Drupal.ajax(false, $link, this.element_settings);
var self_settings = $.extend({}, this.element_settings, {
submit: viewData,
base: false,
element: $link
});
this.pagerAjax = Drupal.ajax(self_settings);
};
Drupal.AjaxCommands.prototype.viewsScrollTop = function (ajax, response) {
......
......@@ -84,12 +84,14 @@
// Bind AJAX behaviors to all items showing the class.
$('a.views-ajax-link', context).once('views-ajax').each(function () {
var element_settings = base_element_settings;
element_settings.base = base;
element_settings.element = this;
// Set the URL to go to the anchor.
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);
Drupal.ajax(element_settings);
});
$('div#views-live-preview a')
......@@ -108,8 +110,10 @@
element_settings.wrapper = 'views-preview-wrapper';
element_settings.method = 'replaceWith';
element_settings.base = base;
element_settings.element = this;
var base = $(this).attr('id');
Drupal.ajax[base] = new Drupal.ajax(base, this, element_settings);
Drupal.ajax(element_settings);
});
// Within a live preview, make exposed widget form buttons re-trigger the
......@@ -132,9 +136,11 @@
element_settings.wrapper = 'views-preview-wrapper';
element_settings.method = 'replaceWith';
element_settings.event = 'click';
element_settings.base = base;
element_settings.element = this;
var base = $(this).attr('id');
Drupal.ajax[base] = new Drupal.ajax(base, this, element_settings);
Drupal.ajax(element_settings);
});
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment