diff --git a/core/misc/tableheader.js b/core/misc/tableheader.js
index 364cad2..c5cb1ac 100644
--- a/core/misc/tableheader.js
+++ b/core/misc/tableheader.js
@@ -174,8 +174,9 @@ $.extend(TableHeader.prototype, {
* Create the duplicate header.
*/
createSticky: function () {
- // Clone the table header so it inherits original jQuery properties.
- var $stickyHeader = this.$originalHeader.clone(true);
+ // Deep clone the table header so it and its children inherit their original
+ // jQuery properties.
+ var $stickyHeader = this.$originalHeader.clone(true, true);
// Hide the table to avoid a flash of the header clone upon page load.
this.$stickyTable = $('
')
.css({
@@ -190,6 +191,9 @@ $.extend(TableHeader.prototype, {
// Initialize all computations.
this.recalculateSticky();
+
+ // Let other scripts know a sticky table header was created.
+ this.$stickyTable.trigger('stickyTableCreated');
},
/**
diff --git a/core/misc/tableselect.js b/core/misc/tableselect.js
index 36af864..3dab15f 100644
--- a/core/misc/tableselect.js
+++ b/core/misc/tableselect.js
@@ -1,93 +1,258 @@
-(function ($) {
+(function ($, Drupal) {
"use strict";
-Drupal.behaviors.tableSelect = {
- attach: function (context, settings) {
- // Select the inner-most table in case of nested tables.
- $(context).find('th.select-all').closest('table').once('table-select', Drupal.tableSelect);
+Drupal.behaviors.TableSelect = {
+ attach: function (context) {
+ var $selectAllHeaders = $(context).find('th.select-all');
+ tableSelectInitHandler($selectAllHeaders);
+ },
+
+ detach: function (context) {
+ var tables = TableSelect.tables;
+ var $context = $(context);
+
+ // Go through all the TabeSelect instances.
+ for (var i = 0, il = tables.length; i < il; i++) {
+ var $selectAll = $context.find(tables[i].$selectAll);
+
+ // Only remove if 'select all' was found in the given context.
+ if ($selectAll.length) {
+ var $table = tables[i].$table;
+ // Unbind all events which are not directly bound to the 'select all'
+ // checkbox.
+ $table.off('.TableSelect');
+ $table.parent().off('.TableSelect');
+
+ // Remove the 'select all' checkbox. This will also remove any events
+ // which are directly bound to the 'select all' checkbox.
+ $selectAll.remove();
+
+ // Trigger an event letting other scripts know TableSelect was removed.
+ $table.trigger('TableSelectRemoved');
+ }
+ }
}
};
-Drupal.tableSelect = function () {
- // Do not add a "Select all" checkbox if there are no rows with checkboxes in the table
- if ($(this).find('td input:checkbox').length === 0) {
- return;
- }
+function tableSelectInitHandler($selectAllHeaders) {
+ $selectAllHeaders.each(function() {
+ var $header = $(this);
+ var $table = $header.closest('table');
- // Keep track of the table, which checkbox is checked and alias the settings.
- var table = this, checkboxes, lastChecked;
- var $table = $(table);
- var strings = { 'selectAll': Drupal.t('Select all rows in this table'), 'selectNone': Drupal.t('Deselect all rows in this table') };
- var updateSelectAll = function (state) {
- // Update table's select-all checkbox (and sticky header's if available).
- $table.prev('table.sticky-header').andSelf().find('th.select-all input:checkbox').each(function() {
- $(this).attr('title', state ? strings.selectNone : strings.selectAll);
- this.checked = state;
- });
- };
-
- // Find all
with class select-all, and insert the check all checkbox.
- $table.find('th.select-all').prepend($('').attr('title', strings.selectAll)).click(function (event) {
- if ($(event.target).is('input:checkbox')) {
- // Loop through all checkboxes and set their state to the select all checkbox' state.
- checkboxes.each(function () {
- this.checked = event.target.checked;
- // Either add or remove the selected class based on the state of the check all checkbox.
- $(this).closest('tr').toggleClass('selected', this.checked);
+ // Do nothing if there are no rows with checkboxes in the table.
+ if ($table.find('td input:checkbox').length) {
+ $table.once('table-select', function() {
+ TableSelect.tables.push(new TableSelect($table, $header));
});
- // Update the title and the state of the check all box.
- updateSelectAll(event.target.checked);
}
});
+}
+
+/**
+ * Constructor for the TableSelect object.
+ *
+ * TableSelect will add a 'select all' checkbox to header of the current table.
+ *
+ * @param $table
+ * jQuery object for the parent table to add a 'select all' checkbox to.
+ * @param $header
+ * jQuery object for the header to add a 'select all' checkbox to.
+ *
+ * @constructor
+ */
+function TableSelect($table, $header) {
+ // Cached jQuery selectors.
+ this.$table = $table;
+ this.$header = $header;
+ this.$selectAll = null;
+
+ // DOM collections.
+ this.checkboxes = null;
+ this.rows = null;
+
+ // Keep track of 'select all' state.
+ this.selectAllState = null;
+
+ // Keep track of checkbox properties.
+ this.checkboxChecked = null;
+ this.checkboxMaxChecked = null;
+ this.lastState = null;
+ this.lastIndex = null;
+
+ // Create 'select all' checkbox and behaviour.
+ this.createSelectAll();
+}
+
+/**
+ * Store the state of TableSelect.
+ */
+$.extend(TableSelect, {
+ /**
+ * This will store the state of all processed tables.
+ *
+ * @type {Array}
+ */
+ tables: []
+});
+
+/**
+ * Extend TableSelect prototype.
+ */
+$.extend(TableSelect.prototype, {
+ titleSelectAll: {
+ all: Drupal.t('Select all rows in this table'),
+ none: Drupal.t('Deselect all rows in this table')
+ },
+
+ createSelectAll: function () {
+ this.checkboxes = [];
+ this.rows = [];
+ this.lastState = [];
+ this.checkboxChecked = 0;
+ this.checkboxMaxChecked = 0;
+
+ // Store the checkboxes, initial state and their parent rows.
+ var $checkboxes = this.$table.find('td input:checkbox');
+ for (var i = 0, il = $checkboxes.length; i < il; i++) {
+ // Reference to heckbox DOM node.
+ this.checkboxes[i] = $checkboxes[i];
+
+ // Only count enabled checkboxes.
+ if (!$checkboxes[i].disabled) {
+ this.checkboxMaxChecked++;
+ if ($checkboxes[i].checked) {
+ this.checkboxChecked++;
+ }
+ }
+
+ // Keep track of the 'checked' state of checkboxes.
+ this.lastState[i] = $checkboxes[i].checked;
+
+ // Reference to parent table row DOM node.
+ this.rows[i] = $($checkboxes[i]).closest('tr')[0];
+ }
+
+ // Create the 'select all' checkbox.
+ this.$selectAll = $('');
+ this.$selectAll.on('click.TableSelect', {TableSelect: this}, this.toggleSelectAll);
- // For each of the checkboxes within the table that are not disabled.
- checkboxes = $table.find('td input:checkbox:enabled').click(function (e) {
- // Either add or remove the selected class based on the state of the check all checkbox.
- $(this).closest('tr').toggleClass('selected', this.checked);
+ // Update the title and selected state of the 'select all' checkbox.
+ this.updateSelectAll();
+
+ // Add the 'select all' checkbox to the table header.
+ this.$selectAll.prependTo(this.$header);
+
+ // Listen for the creation of sticky tables. Do this on the direct parent of
+ // the table, to minimise the actual execution of the following function.
+ this.$table.parent().on('stickyTableCreated.TableSelect', {TableSelect: this}, this.adaptToSticky);
+
+ // Keep track of the checkboxes state.
+ this.$table.on('click.TableSelect', 'td input:checkbox', {TableSelect: this}, this.toggleCheckbox);
+
+ // Let other scripts know a 'select all' checkbox was created.
+ this.$table.trigger('TableSelectCreated');
+ },
+
+ adaptToSticky: function (e) {
+ var self = e.data.TableSelect;
+ var $stickyTable = self.$table.prev('table.sticky-header');
+
+ // If there is a sticky table header attached to the current table, add the
+ // sticky table 'select all' checkbox to the cached 'select all' selector.
+ if ($stickyTable.length) {
+ var $stickyCheckbox = $stickyTable.find('th.select-all input:checkbox');
+ self.$selectAll = $([self.$selectAll[0], $stickyCheckbox[0]]);
+ }
+ },
+
+ toggleSelectAll: function (e) {
+ var self = e.data.TableSelect;
+ var state = $(this).prop('checked');
+
+ // Update the title and selected state of the 'select all' checkbox.
+ self.updateSelectAll(state);
+
+ // Toggle all checkboxes.
+ self.toggleCheckboxes(0, self.checkboxes.length - 1, state);
+ },
+
+ updateSelectAll: function (state) {
+ // If the state parameter was omitted, find a default state value based on
+ // the number of checkboxes selected.
+ if (typeof state === 'undefined') {
+ // If all checkboxes are checked, make sure the select-all one is checked
+ // too, otherwise keep it unchecked.
+ state = (this.checkboxChecked === this.checkboxMaxChecked) ? true : false;
+ }
+
+ // Only do the actual updating if the 'select all' checkbox does not have
+ // the desired state already.
+ if (this.selectAllState !== state) {
+ // Set the title and 'checked' state explicitly in the event there are
+ // multiple checkboxes controlling the same table. Otherwise these
+ // checkboxes would not be in sync.
+ var title = state ? this.titleSelectAll.none : this.titleSelectAll.all;
+ this.$selectAll.attr('title', title).prop('checked', state);
+ this.selectAllState = state;
+ }
+ },
+
+ toggleCheckbox: function (e) {
+ var self = e.data.TableSelect;
+ var currentIndex = $(self.checkboxes).index(this);
+ var lastIndex = self.lastIndex;
// If this is a shift click, we need to highlight everything in the range.
// Also make sure that we are actually checking checkboxes over a range and
// that a checkbox has been checked or unchecked before.
- if (e.shiftKey && lastChecked && lastChecked !== e.target) {
- // We use the checkbox's parent TR to do our range searching.
- Drupal.tableSelectRange($(e.target).closest('tr')[0], $(lastChecked).closest('tr')[0], e.target.checked);
+ var from, to;
+ if (e.shiftKey && lastIndex !== null && lastIndex !== currentIndex) {
+ from = (lastIndex < currentIndex) ? lastIndex : currentIndex;
+ to = (lastIndex < currentIndex) ? currentIndex : lastIndex;
}
+ else {
+ from = currentIndex;
+ to = currentIndex;
+ }
+ self.toggleCheckboxes(from, to, this.checked);
- // If all checkboxes are checked, make sure the select-all one is checked too, otherwise keep unchecked.
- updateSelectAll((checkboxes.length === checkboxes.filter(':checked').length));
+ // Update the most recent toggled checkbox.
+ self.lastIndex = currentIndex;
- // Keep track of the last checked checkbox.
- lastChecked = e.target;
- });
-};
+ // Update the title and selected state of the 'select all' checkbox.
+ self.updateSelectAll();
+ },
-Drupal.tableSelectRange = function (from, to, state) {
- // We determine the looping mode based on the the order of from and to.
- var mode = from.rowIndex > to.rowIndex ? 'previousSibling' : 'nextSibling';
+ toggleCheckboxes: function (from, to, state) {
+ var checkboxes = this.checkboxes;
+ var rows = this.rows;
+ var lastState = this.lastState;
+ var checkboxChecked = this.checkboxChecked;
- // Traverse through the sibling nodes.
- for (var i = from[mode], $i; i; i = i[mode]) {
- // Make sure that we're only dealing with elements.
- if (i.nodeType !== 1) {
- continue;
- }
- $i = $(i);
- // Either add or remove the selected class based on the state of the target checkbox.
- $i.toggleClass('selected', state);
- $i.find('input:checkbox').attr('checked', state);
-
- if (to.nodeType) {
- // If we are at the end of the range, stop.
- if (i === to) {
- break;
+ // Gather all the non-disabled checkboxes that need to be toggled.
+ var applyCheckboxes = [];
+ var applyRows = [];
+ for (var i = from, il = to; i <= il; i++) {
+ if (!checkboxes[i].disabled) {
+ applyCheckboxes.push(checkboxes[i]);
+ applyRows.push(rows[i]);
+ if (lastState[i] !== state) {
+ checkboxChecked += state ? 1 : -1;
+ }
+ lastState[i] = state;
}
}
- // A faster alternative to doing $(i).filter(to).length.
- else if ($.filter(to, [i]).r.length) {
- break;
- }
+ this.lastState = lastState;
+ this.checkboxChecked = checkboxChecked;
+
+ // Toggle both the 'checked' property and the 'selected' class.
+ $(applyCheckboxes).prop('checked', state);
+ $(applyRows).toggleClass('selected', state);
}
-};
+});
+
+// Expose constructor in the public space.
+Drupal.TableSelect = TableSelect;
-})(jQuery);
+}(jQuery, Drupal));