diff --git a/includes/common.inc b/includes/common.inc index b5686cab7ebd94bbf80f8b97d98544d2a1ed7bf8..ce21e4fa49a57bcd79033d4247a2842f8e32b0ee 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 87e724d4671e8a080d57fd1431c92ea26af67435..489e2fb1b5618a46181b73adcb5165c78366fbb1 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 8e53c210af652b8f44fce0c25f42297c08c7ca8b..d0a32f2a1210f3aa97769d9e1a131b88b8f1ee90 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 c4fac9be3ce539d5c0ca1a0dfea9f9b0ffa10114..741f4b0bae382054d36fa14d88f70fa32d535a9b 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($('<div class="fieldset-wrapper"></div>') - .append(fieldset.children(':not(legend):not(.action)')) - ).addClass('collapse-processed'); + .after( + $('<div class="fieldset-wrapper"></div>') + .append(fieldset.children(':not(legend):not(.action)')) + ); }); } }; diff --git a/misc/drupal.js b/misc/drupal.js index dbd057bb914db8f55af6d44218295ad86ecae86a..ea688a499a3843ff0396d6eb7a83fe4fdcf04044 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 a59451760ce2b20bad5906027325dd42af43aeee..ca572cf38fcbc0c676c070ebafc3d8196822a0c1 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 db40f2437ef25b83a4e8f3796a78ed4ffe7b15ae..0c7acbc751ee834459748c795471a8c92b534006 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 a49dd2d401646e8ca381edd1b57b0ccd6e451405..52f6b6d33e3909247560b5394bccd0b107d42da3 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('<table class="sticky-header"></table>').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 332a1d9e5566582db3cc9a96c8ab268876ff3e60..cba0c57a5fba82b173389849d18e81bc67344549 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 cec9abfff71530602d37986959b76ead6d2e99f7..5938b9e4db69d4765ecc83c5c0b4d4eacac96f5d 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('<div class="resizable-textarea"><span></span></div>') - .parent().append($('<div class="grippie"></div>').mousedown(startDrag)); + var staticOffset = null; + var textarea = $(this).wrap('<div class="resizable-textarea"><span></span></div>'); + var grippie = $('<div class="grippie"></div>').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 a1e689c92ffbd548e130e0f12ee626f4c2212e78..b708875f5bf22a37b33d42669b5c78b52779ff79 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 7fb15549f5ae7552517faf3600289ba999a343db..32c4ee697307aa2765807a9edffc2656168ab3af 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 34c04d2460c6902945b95aeff472f06947c323cf..e83f6ea04e8be956438fb0822aa6b194a9ef0806 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 c286c40a9ea4791ff5b4827c70cdd86e00123c2a..a834f11edac7e47313094058cc011f1f2053c0c7 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('<div id="gradient"></div>') - .addClass('color-processed'); + $('#preview').once('color').append('<div id="gradient"></div>'); 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 bdd1c3b952a799fc034e06d0876e5fba7ecdb9f6..58f00dc6b526731774e35855874bf5e2eba35808 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 11aa8b4b8cbaf2469b97f9f4be758f553b84bd15..4da80811d9dfed31df0db1ca820842e03efff460 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 b975c859787ab924a773d3f1ebe62a58dc117658..d9a29a0cb5226eaa4688a6a19bc4effcf7eaba67 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 7b804182f9fc74e68ec0632f961ba4cdfe31b464..c1901003939938cb1e812fe87286eb5b6877bd08 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 9d75911fae2f16628e4beb5eda690694b8a68214..d382375ab7a80dbe48855afdbbc8dfbd942b3726 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();