Chris@0: /**
Chris@0: * @file
Chris@0: * Table select functionality.
Chris@0: */
Chris@0:
Chris@17: (function($, Drupal) {
Chris@0: /**
Chris@0: * Initialize tableSelects.
Chris@0: *
Chris@0: * @type {Drupal~behavior}
Chris@0: *
Chris@0: * @prop {Drupal~behaviorAttach} attach
Chris@0: * Attaches tableSelect functionality.
Chris@0: */
Chris@0: Drupal.behaviors.tableSelect = {
Chris@0: attach(context, settings) {
Chris@0: // Select the inner-most table in case of nested tables.
Chris@14: $(context)
Chris@14: .find('th.select-all')
Chris@14: .closest('table')
Chris@14: .once('table-select')
Chris@14: .each(Drupal.tableSelect);
Chris@0: },
Chris@0: };
Chris@0:
Chris@0: /**
Chris@0: * Callback used in {@link Drupal.behaviors.tableSelect}.
Chris@0: */
Chris@17: Drupal.tableSelect = function() {
Chris@0: // Do not add a "Select all" checkbox if there are no rows with checkboxes
Chris@0: // in the table.
Chris@0: if ($(this).find('td input[type="checkbox"]').length === 0) {
Chris@0: return;
Chris@0: }
Chris@0:
Chris@0: // Keep track of the table, which checkbox is checked and alias the
Chris@0: // settings.
Chris@0: const table = this;
Chris@0: let checkboxes;
Chris@0: let lastChecked;
Chris@0: const $table = $(table);
Chris@0: const strings = {
Chris@0: selectAll: Drupal.t('Select all rows in this table'),
Chris@0: selectNone: Drupal.t('Deselect all rows in this table'),
Chris@0: };
Chris@17: const updateSelectAll = function(state) {
Chris@0: // Update table's select-all checkbox (and sticky header's if available).
Chris@17: $table
Chris@17: .prev('table.sticky-header')
Chris@17: .addBack()
Chris@17: .find('th.select-all input[type="checkbox"]')
Chris@17: .each(function() {
Chris@17: const $checkbox = $(this);
Chris@17: const stateChanged = $checkbox.prop('checked') !== state;
Chris@0:
Chris@17: $checkbox.attr(
Chris@17: 'title',
Chris@17: state ? strings.selectNone : strings.selectAll,
Chris@17: );
Chris@0:
Chris@0: /**
Chris@0: * @checkbox {HTMLElement}
Chris@0: */
Chris@0: if (stateChanged) {
Chris@17: $checkbox.prop('checked', state).trigger('change');
Chris@0: }
Chris@17: });
Chris@17: };
Chris@0:
Chris@17: // Find all
with class select-all, and insert the check all checkbox.
Chris@17: $table
Chris@17: .find('th.select-all')
Chris@17: .prepend(
Chris@17: $('').attr(
Chris@17: 'title',
Chris@17: strings.selectAll,
Chris@17: ),
Chris@17: )
Chris@17: .on('click', event => {
Chris@17: if ($(event.target).is('input[type="checkbox"]')) {
Chris@17: // Loop through all checkboxes and set their state to the select all
Chris@17: // checkbox' state.
Chris@17: checkboxes.each(function() {
Chris@17: const $checkbox = $(this);
Chris@17: const stateChanged =
Chris@17: $checkbox.prop('checked') !== event.target.checked;
Chris@17:
Chris@17: /**
Chris@17: * @checkbox {HTMLElement}
Chris@17: */
Chris@17: if (stateChanged) {
Chris@17: $checkbox.prop('checked', event.target.checked).trigger('change');
Chris@17: }
Chris@17: // Either add or remove the selected class based on the state of the
Chris@17: // check all checkbox.
Chris@17:
Chris@17: /**
Chris@17: * @checkbox {HTMLElement}
Chris@17: */
Chris@17: $checkbox.closest('tr').toggleClass('selected', this.checked);
Chris@17: });
Chris@17: // Update the title and the state of the check all box.
Chris@17: updateSelectAll(event.target.checked);
Chris@17: }
Chris@17: });
Chris@0:
Chris@0: // For each of the checkboxes within the table that are not disabled.
Chris@17: checkboxes = $table
Chris@17: .find('td input[type="checkbox"]:enabled')
Chris@17: .on('click', function(e) {
Chris@17: // Either add or remove the selected class based on the state of the
Chris@17: // check all checkbox.
Chris@0:
Chris@17: /**
Chris@17: * @this {HTMLElement}
Chris@17: */
Chris@17: $(this)
Chris@17: .closest('tr')
Chris@17: .toggleClass('selected', this.checked);
Chris@0:
Chris@17: // If this is a shift click, we need to highlight everything in the
Chris@17: // range. Also make sure that we are actually checking checkboxes
Chris@17: // over a range and that a checkbox has been checked or unchecked before.
Chris@17: if (e.shiftKey && lastChecked && lastChecked !== e.target) {
Chris@17: // We use the checkbox's parent
to do our range searching.
Chris@17: Drupal.tableSelectRange(
Chris@17: $(e.target).closest('tr')[0],
Chris@17: $(lastChecked).closest('tr')[0],
Chris@17: e.target.checked,
Chris@17: );
Chris@17: }
Chris@0:
Chris@17: // If all checkboxes are checked, make sure the select-all one is checked
Chris@17: // too, otherwise keep unchecked.
Chris@17: updateSelectAll(
Chris@17: checkboxes.length === checkboxes.filter(':checked').length,
Chris@17: );
Chris@0:
Chris@17: // Keep track of the last checked checkbox.
Chris@17: lastChecked = e.target;
Chris@17: });
Chris@0:
Chris@0: // If all checkboxes are checked on page load, make sure the select-all one
Chris@0: // is checked too, otherwise keep unchecked.
Chris@17: updateSelectAll(checkboxes.length === checkboxes.filter(':checked').length);
Chris@0: };
Chris@0:
Chris@0: /**
Chris@0: * @param {HTMLElement} from
Chris@0: * The HTML element representing the "from" part of the range.
Chris@0: * @param {HTMLElement} to
Chris@0: * The HTML element representing the "to" part of the range.
Chris@0: * @param {bool} state
Chris@0: * The state to set on the range.
Chris@0: */
Chris@17: Drupal.tableSelectRange = function(from, to, state) {
Chris@0: // We determine the looping mode based on the order of from and to.
Chris@17: const mode =
Chris@17: from.rowIndex > to.rowIndex ? 'previousSibling' : 'nextSibling';
Chris@0:
Chris@0: // Traverse through the sibling nodes.
Chris@0: for (let i = from[mode]; i; i = i[mode]) {
Chris@14: const $i = $(i);
Chris@0: // Make sure that we're only dealing with elements.
Chris@0: if (i.nodeType !== 1) {
Chris@0: continue;
Chris@0: }
Chris@0: // Either add or remove the selected class based on the state of the
Chris@0: // target checkbox.
Chris@0: $i.toggleClass('selected', state);
Chris@0: $i.find('input[type="checkbox"]').prop('checked', state);
Chris@0:
Chris@0: if (to.nodeType) {
Chris@0: // If we are at the end of the range, stop.
Chris@0: if (i === to) {
Chris@0: break;
Chris@0: }
Chris@0: }
Chris@0: // A faster alternative to doing $(i).filter(to).length.
Chris@0: else if ($.filter(to, [i]).r.length) {
Chris@0: break;
Chris@0: }
Chris@0: }
Chris@0: };
Chris@17: })(jQuery, Drupal);