diff --git a/includes/common.inc b/includes/common.inc
index b5686cab7ebd..ce21e4fa49a5 100644
--- a/includes/common.inc
+++ b/includes/common.inc
@@ -2960,8 +2960,9 @@ function drupal_add_js($data = NULL, $options = NULL) {
'preprocess' => TRUE,
),
);
- // jQuery itself is registered as a library.
+ // Register all required libraries.
drupal_add_library('system', 'jquery');
+ drupal_add_library('system', 'once');
}
switch ($options['type']) {
diff --git a/misc/autocomplete.js b/misc/autocomplete.js
index 87e724d4671e..489e2fb1b561 100644
--- a/misc/autocomplete.js
+++ b/misc/autocomplete.js
@@ -7,7 +7,7 @@
Drupal.behaviors.autocomplete = {
attach: function (context, settings) {
var acdb = [];
- $('input.autocomplete:not(.autocomplete-processed)', context).each(function () {
+ $('input.autocomplete', context).once('autocomplete', function () {
var uri = this.value;
if (!acdb[uri]) {
acdb[uri] = new Drupal.ACDB(uri);
@@ -16,7 +16,6 @@ Drupal.behaviors.autocomplete = {
.attr('autocomplete', 'OFF')[0];
$(input.form).submit(Drupal.autocompleteSubmit);
new Drupal.jsAC(input, acdb[uri]);
- $(this).addClass('autocomplete-processed');
});
}
};
diff --git a/misc/batch.js b/misc/batch.js
index 8e53c210af65..d0a32f2a1210 100644
--- a/misc/batch.js
+++ b/misc/batch.js
@@ -6,11 +6,7 @@
*/
Drupal.behaviors.batch = {
attach: function (context, settings) {
- // This behavior attaches by ID, so is only valid once on a page.
- if ($('#progress.batch-processed').size()) {
- return;
- }
- $('#progress', context).addClass('batch-processed').each(function () {
+ $('#progress', context).once('batch', function () {
var holder = $(this);
// Success: redirect to the summary.
diff --git a/misc/collapse.js b/misc/collapse.js
index c4fac9be3ce5..741f4b0bae38 100644
--- a/misc/collapse.js
+++ b/misc/collapse.js
@@ -53,7 +53,7 @@ Drupal.collapseScrollIntoView = function (node) {
Drupal.behaviors.collapse = {
attach: function (context, settings) {
- $('fieldset.collapsible > legend:not(.collapse-processed)', context).each(function () {
+ $('fieldset.collapsible > legend', context).once('collapse', function () {
var fieldset = $(this.parentNode);
// Expand if there are errors inside
if ($('input.error, textarea.error, select.error', fieldset).size() > 0) {
@@ -81,9 +81,10 @@ Drupal.behaviors.collapse = {
return false;
}))
.append(summary)
- .after($('
')
- .append(fieldset.children(':not(legend):not(.action)'))
- ).addClass('collapse-processed');
+ .after(
+ $('')
+ .append(fieldset.children(':not(legend):not(.action)'))
+ );
});
}
};
diff --git a/misc/drupal.js b/misc/drupal.js
index dbd057bb914d..ea688a499a38 100644
--- a/misc/drupal.js
+++ b/misc/drupal.js
@@ -12,7 +12,6 @@ if ($ === undefined) {
};
}
-
(function ($) {
/**
@@ -38,10 +37,15 @@ if ($ === undefined) {
* loaded, feeding in an element to be processed, in order to attach all
* behaviors to the new content.
*
- * Behaviors should use a class in the form behaviorName-processed to ensure
- * the behavior is attached only once to a given element. (Doing so enables
- * the reprocessing of given elements, which may be needed on occasion despite
- * the ability to limit behavior attachment to a particular element.)
+ * Behaviors should use
+ * @code
+ * $(selector).once('behavior-name', function () {
+ * ...
+ * });
+ * @endcode
+ * to ensure the behavior is attached only once to a given element. (Doing so
+ * enables the reprocessing of given elements, which may be needed on occasion
+ * despite the ability to limit behavior attachment to a particular element.)
*
* @param context
* An element to attach behaviors to. If none is given, the document element
diff --git a/misc/form.js b/misc/form.js
index a59451760ce2..ca572cf38fcb 100644
--- a/misc/form.js
+++ b/misc/form.js
@@ -61,8 +61,7 @@ Drupal.behaviors.formUpdated = {
Drupal.behaviors.multiselectSelector = {
attach: function (context, settings) {
// Automatically selects the right radio button in a multiselect control.
- $('.multiselect select:not(.multiselectSelector-processed)', context)
- .addClass('multiselectSelector-processed').change(function () {
+ $('.multiselect select', context).once('multiselect').change(function () {
$('.multiselect input:radio[value="' + this.id.substr(5) + '"]')
.attr('checked', true);
});
@@ -75,8 +74,7 @@ Drupal.behaviors.multiselectSelector = {
*/
Drupal.behaviors.filterGuidelines = {
attach: function (context) {
- $('.filter-guidelines:not(.filter-guidelines-processed)', context)
- .addClass('filter-guidelines-processed')
+ $('.filter-guidelines', context).once('filter-guidelines')
.find('label').hide()
.parents('.filter-wrapper').find('select.filter-list')
.bind('change', function () {
diff --git a/misc/tabledrag.js b/misc/tabledrag.js
index db40f2437ef2..0c7acbc751ee 100644
--- a/misc/tabledrag.js
+++ b/misc/tabledrag.js
@@ -15,17 +15,11 @@
Drupal.behaviors.tableDrag = {
attach: function (context, settings) {
for (var base in settings.tableDrag) {
- if (!$('#' + base + '.tabledrag-processed', context).size()) {
- var tableSettings = settings.tableDrag[base];
-
- $('#' + base).filter(':not(.tabledrag-processed)').each(function () {
- // Create the new tableDrag instance. Save in the Drupal variable
- // to allow other scripts access to the object.
- Drupal.tableDrag[base] = new Drupal.tableDrag(this, tableSettings);
- });
-
- $('#' + base).addClass('tabledrag-processed');
- }
+ $('#' + base, context).once('tabledrag', function () {
+ // Create the new tableDrag instance. Save in the Drupal variable
+ // to allow other scripts access to the object.
+ Drupal.tableDrag[base] = new Drupal.tableDrag(this, settings.tableDrag[base]);
+ });
}
}
};
diff --git a/misc/tableheader.js b/misc/tableheader.js
index a49dd2d40164..52f6b6d33e39 100644
--- a/misc/tableheader.js
+++ b/misc/tableheader.js
@@ -17,7 +17,7 @@ Drupal.behaviors.tableHeader = {
// Keep track of all cloned table headers.
var headers = [];
- $('table.sticky-enabled thead:not(.tableHeader-processed)', context).each(function () {
+ $('table.sticky-enabled thead', context).once('tableheader', function () {
// Clone thead so it inherits original jQuery properties.
var headerClone = $(this).clone(true).insertBefore(this.parentNode).wrap('').parent().css({
position: 'fixed',
@@ -34,7 +34,6 @@ Drupal.behaviors.tableHeader = {
tracker(headerClone);
$(table).addClass('sticky-table');
- $(this).addClass('tableHeader-processed');
});
// Define the anchor holding var.
@@ -81,11 +80,10 @@ Drupal.behaviors.tableHeader = {
// Only attach to scrollbars once, even if Drupal.attachBehaviors is called
// multiple times.
- if (!$('body').hasClass('tableHeader-processed')) {
- $('body').addClass('tableHeader-processed');
+ $('body').once(function () {
$(window).scroll(Drupal.tableHeaderDoScroll);
$(document.documentElement).scroll(Drupal.tableHeaderDoScroll);
- }
+ });
// Track scrolling.
Drupal.tableHeaderOnScroll = function () {
diff --git a/misc/tableselect.js b/misc/tableselect.js
index 332a1d9e5566..cba0c57a5fba 100644
--- a/misc/tableselect.js
+++ b/misc/tableselect.js
@@ -3,7 +3,7 @@
Drupal.behaviors.tableSelect = {
attach: function (context, settings) {
- $('form table:has(th.select-all):not(.tableSelect-processed)', context).each(Drupal.tableSelect);
+ $('form table:has(th.select-all)', context).once('table-select', Drupal.tableSelect);
}
};
@@ -56,7 +56,6 @@ Drupal.tableSelect = function () {
// Keep track of the last checked checkbox.
lastChecked = e.target;
});
- $(this).addClass('tableSelect-processed');
};
Drupal.tableSelectRange = function (from, to, state) {
diff --git a/misc/textarea.js b/misc/textarea.js
index cec9abfff715..5938b9e4db69 100644
--- a/misc/textarea.js
+++ b/misc/textarea.js
@@ -3,20 +3,16 @@
Drupal.behaviors.textarea = {
attach: function (context, settings) {
- $('textarea.resizable:not(.textarea-processed)', context).each(function () {
- // Avoid non-processed teasers.
- if ($(this).is(('textarea.teaser:not(.teaser-processed)'))) {
- return false;
- }
- var textarea = $(this).addClass('textarea-processed'), staticOffset = null;
-
+ $('textarea.resizable', context).once('textarea', function () {
// When wrapping the text area, work around an IE margin bug. See:
// http://jaspan.com/ie-inherited-margin-bug-form-elements-and-haslayout
- $(this).wrap('
')
- .parent().append($('').mousedown(startDrag));
+ var staticOffset = null;
+ var textarea = $(this).wrap('
');
+ var grippie = $('').mousedown(startDrag);
- var grippie = $('div.grippie', $(this).parent())[0];
- grippie.style.marginRight = (grippie.offsetWidth - $(this)[0].offsetWidth) + 'px';
+ grippie
+ .insertAfter(textarea)
+ .css('margin-right', grippie.width() - textarea.width());
function startDrag(e) {
staticOffset = textarea.height() - e.pageY;
diff --git a/misc/timezone.js b/misc/timezone.js
index a1e689c92ffb..b708875f5bf2 100644
--- a/misc/timezone.js
+++ b/misc/timezone.js
@@ -6,7 +6,7 @@
*/
Drupal.behaviors.setTimezone = {
attach: function (context, settings) {
- $('select.timezone-detect:not(.timezone-processed)', context).addClass('timezone-processed').each(function () {
+ $('select.timezone-detect', context).once('timezone', function () {
var dateString = Date();
// In some client environments, date strings include a time zone
// abbreviation, between 3 and 5 letters enclosed in parentheses,
diff --git a/misc/vertical-tabs.js b/misc/vertical-tabs.js
index 7fb15549f5ae..32c4ee697307 100644
--- a/misc/vertical-tabs.js
+++ b/misc/vertical-tabs.js
@@ -15,7 +15,7 @@
*/
Drupal.behaviors.verticalTabs = {
attach: function (context) {
- $('.vertical-tabs-panes:not(.vertical-tabs-processed)', context).each(function () {
+ $('.vertical-tabs-panes', context).once('vertical-tabs', function () {
var focusID = $(':hidden.vertical-tabs-active-tab', this).val();
var focus;
// Create the tab column.
@@ -42,7 +42,7 @@ Drupal.behaviors.verticalTabs = {
focus = $('> .vertical-tabs-pane:first', this);
}
focus.data('verticalTab').focus();
- }).addClass('vertical-tabs-processed');
+ });
}
};
diff --git a/modules/block/block.js b/modules/block/block.js
index 34c04d2460c6..e83f6ea04e8b 100644
--- a/modules/block/block.js
+++ b/modules/block/block.js
@@ -51,7 +51,7 @@ Drupal.behaviors.blockDrag = {
};
// Add the behavior to each region select list.
- $('select.block-region-select:not(.blockregionselect-processed)', context).each(function () {
+ $('select.block-region-select', context).once('block-region-select', function () {
$(this).change(function (event) {
// Make our new row and select field.
var row = $(this).parents('tr:first');
@@ -82,7 +82,6 @@ Drupal.behaviors.blockDrag = {
// Remove focus from selectbox.
select.get(0).blur();
});
- $(this).addClass('blockregionselect-processed');
});
var checkEmptyRegions = function (table, rowObject) {
diff --git a/modules/color/color.js b/modules/color/color.js
index c286c40a9ea4..a834f11edac7 100644
--- a/modules/color/color.js
+++ b/modules/color/color.js
@@ -4,10 +4,10 @@
Drupal.behaviors.color = {
attach: function (context, settings) {
// This behavior attaches by ID, so is only valid once on a page.
- if ($('#color_scheme_form .color-form.color-processed').size()) {
+ var form = $('#system-theme-settings .color-form', context).once('color');
+ if (form.length == 0) {
return;
}
- var form = $('#system-theme-settings .color-form', context);
var inputs = [];
var hooks = [];
var locks = [];
@@ -24,9 +24,7 @@ Drupal.behaviors.color = {
}
// Build a preview.
- $('#preview:not(.color-processed)')
- .append('')
- .addClass('color-processed');
+ $('#preview').once('color').append('');
var gradient = $('#preview #gradient');
var h = parseInt(gradient.css('height')) / 10;
for (i = 0; i < h; ++i) {
diff --git a/modules/comment/comment.js b/modules/comment/comment.js
index bdd1c3b952a7..58f00dc6b526 100644
--- a/modules/comment/comment.js
+++ b/modules/comment/comment.js
@@ -6,9 +6,7 @@ Drupal.behaviors.comment = {
$.each(['name', 'homepage', 'mail'], function () {
var cookie = Drupal.comment.getCookie('comment_info_' + this);
if (cookie) {
- $('#comment-form input[name=' + this + ']:not(.comment-processed)', context)
- .val(cookie)
- .addClass('comment-processed');
+ $('#comment-form input[name=' + this + ']', context).once('comment').val(cookie);
}
});
}
diff --git a/modules/system/system.js b/modules/system/system.js
index 11aa8b4b8cba..4da80811d9df 100644
--- a/modules/system/system.js
+++ b/modules/system/system.js
@@ -32,7 +32,7 @@ Drupal.behaviors.cleanURLsSettingsCheck = {
// This behavior attaches by ID, so is only valid once on a page.
// Also skip if we are on an install page, as Drupal.cleanURLsInstallCheck will handle
// the processing.
- if (!($('#edit-clean-url').size()) || $('.clean-url-processed, #edit-clean-url.install').size()) {
+ if (!($('#edit-clean-url').length) || $('#edit-clean-url.install').once('clean-url').length) {
return;
}
var url = settings.basePath + 'admin/config/search/clean-urls/check';
@@ -44,7 +44,6 @@ Drupal.behaviors.cleanURLsSettingsCheck = {
location = settings.basePath +"admin/config/search/clean-urls";
}
});
- $('#clean-url').addClass('clean-url-processed');
}
};
@@ -68,7 +67,6 @@ Drupal.cleanURLsInstallCheck = function () {
$('#edit-clean-url').attr('value', 1);
}
});
- $('#edit-clean-url').addClass('clean-url-processed');
};
/**
@@ -79,21 +77,17 @@ Drupal.cleanURLsInstallCheck = function () {
Drupal.behaviors.copyFieldValue = {
attach: function (context, settings) {
for (var sourceId in settings.copyFieldValue) {
- // Get the list of target fields.
- targetIds = settings.copyFieldValue[sourceId];
- if (!$('#'+ sourceId + '.copy-field-values-processed', context).size()) {
+ $('#' + sourceId, context).once('copy-field-values').bind('blur', function () {
+ // Get the list of target fields.
+ var targetIds = settings.copyFieldValue[sourceId];
// Add the behavior to update target fields on blur of the primary field.
- sourceField = $('#' + sourceId);
- sourceField.bind('blur', function () {
- for (var delta in targetIds) {
- var targetField = $('#'+ targetIds[delta]);
- if (targetField.val() == '') {
- targetField.val(this.value);
- }
+ for (var delta in targetIds) {
+ var targetField = $('#' + targetIds[delta]);
+ if (targetField.val() == '') {
+ targetField.val(this.value);
}
- });
- sourceField.addClass('copy-field-values-processed');
- }
+ }
+ });
}
}
};
@@ -104,12 +98,12 @@ Drupal.behaviors.copyFieldValue = {
Drupal.behaviors.dateTime = {
attach: function (context, settings) {
// Show/hide custom format depending on the select's value.
- $('select.date-format:not(.date-time-processed)', context).change(function () {
- $(this).addClass('date-time-processed').parents('div.date-container').children('div.custom-container')[$(this).val() == 'custom' ? 'show' : 'hide']();
+ $('select.date-format', context).once('date-time').change(function () {
+ $(this).parents('div.date-container').children('div.custom-container')[$(this).val() == 'custom' ? 'show' : 'hide']();
});
// Attach keyup handler to custom format inputs.
- $('input.custom-format:not(.date-time-processed)', context).addClass('date-time-processed').keyup(function () {
+ $('input.custom-format', context).once('date-time').keyup(function () {
var input = $(this);
var url = settings.dateTime.lookup + (settings.dateTime.lookup.match(/\?q=/) ? '&format=' : '?format=') + encodeURIComponent(input.val());
$.getJSON(url, function (data) {
diff --git a/modules/system/system.module b/modules/system/system.module
index b975c859787a..d9a29a0cb522 100644
--- a/modules/system/system.module
+++ b/modules/system/system.module
@@ -933,6 +933,16 @@ function system_library() {
),
);
+ // jQuery Once.
+ $libraries['once'] = array(
+ 'title' => 'jQuery Once',
+ 'website' => 'http://plugins.jquery.com/project/once',
+ 'version' => '1.2',
+ 'js' => array(
+ 'misc/jquery.once.js' => array('weight' => JS_LIBRARY - 19),
+ ),
+ );
+
// jQuery Form Plugin.
$libraries['form'] = array(
'title' => 'jQuery Form Plugin',
diff --git a/modules/toolbar/toolbar.js b/modules/toolbar/toolbar.js
index 7b804182f9fc..c19010039399 100644
--- a/modules/toolbar/toolbar.js
+++ b/modules/toolbar/toolbar.js
@@ -8,18 +8,12 @@ Drupal.behaviors.admin = {
attach: function() {
// Set the intial state of the toolbar.
- $('#toolbar:not(.processed)').each(function() {
- Drupal.admin.toolbar.init();
- $(this).addClass('processed');
- });
+ $('#toolbar', context).once('toolbar', Drupal.admin.toolbar.init);
// Toggling of admin shortcuts visibility.
- $('#toolbar span.toggle:not(.processed)').each(function() {
- $(this).click(function() {
- Drupal.admin.toolbar.toggle();
- return false;
- });
- $(this).addClass('processed');
+ $('#toolbar span.toggle', context).once('toolbar-toggle').click(function() {
+ Drupal.admin.toolbar.toggle();
+ return false;
});
}
};
diff --git a/modules/user/user.js b/modules/user/user.js
index 9d75911fae2f..d382375ab7a8 100644
--- a/modules/user/user.js
+++ b/modules/user/user.js
@@ -8,8 +8,8 @@
Drupal.behaviors.password = {
attach: function (context, settings) {
var translate = settings.password;
- $('input.password-field:not(.password-processed)', context).each(function () {
- var passwordInput = $(this).addClass('password-processed');
+ $('input.password-field', context).once('password', function () {
+ var passwordInput = $(this);
var innerWrapper = $(this).parent();
var outerWrapper = $(this).parent().parent();