tableresponsive.es6.js

/**
 * @file
 * Responsive table functionality.
 */

(function ($, Drupal, window) {
  /**
   * The TableResponsive object optimizes table presentation for screen size.
   *
   * A responsive table hides columns at small screen sizes, leaving the most
   * important columns visible to the end user. Users should not be prevented
   * from accessing all columns, however. This class adds a toggle to a table
   * with hidden columns that exposes the columns. Exposing the columns will
   * likely break layouts, but it provides the user with a means to access
   * data, which is a guiding principle of responsive design.
   *
   * @constructor Drupal.TableResponsive
   *
   * @param {HTMLElement} table
   *   The table element to initialize the responsive table on.
   */
  function TableResponsive(table) {
    this.table = table;
    this.$table = $(table);
    this.showText = Drupal.t('Show all columns');
    this.hideText = Drupal.t('Hide lower priority columns');
    // Store a reference to the header elements of the table so that the DOM is
    // traversed only once to find them.
    this.$headers = this.$table.find('th');
    // Add a link before the table for users to show or hide weight columns.
    this.$link = $(
      '<button type="button" class="link tableresponsive-toggle"></button>',
    )
      .attr(
        'title',
        Drupal.t(
          'Show table cells that were hidden to make the table fit within a small screen.',
        ),
      )
      .on('click', $.proxy(this, 'eventhandlerToggleColumns'));

    this.$table.before(
      $('<div class="tableresponsive-toggle-columns"></div>').append(
        this.$link,
      ),
    );

    // Attach a resize handler to the window.
    $(window)
      .on(
        'resize.tableresponsive',
        $.proxy(this, 'eventhandlerEvaluateColumnVisibility'),
      )
      .trigger('resize.tableresponsive');
  }

  /**
   * Attach the tableResponsive function to {@link Drupal.behaviors}.
   *
   * @type {Drupal~behavior}
   *
   * @prop {Drupal~behaviorAttach} attach
   *   Attaches tableResponsive functionality.
   */
  Drupal.behaviors.tableResponsive = {
    attach(context, settings) {
      once('tableresponsive', 'table.responsive-enabled', context).forEach(
        (table) => {
          TableResponsive.tables.push(new TableResponsive(table));
        },
      );
    },
  };

  /**
   * Extend the TableResponsive function with a list of managed tables.
   */
  $.extend(
    TableResponsive,
    /** @lends Drupal.TableResponsive */ {
      /**
       * Store all created instances.
       *
       * @type {Array.<Drupal.TableResponsive>}
       */
      tables: [],
    },
  );

  /**
   * Associates an action link with the table that will show hidden columns.
   *
   * Columns are assumed to be hidden if their header has the class priority-low
   * or priority-medium.
   */
  $.extend(
    TableResponsive.prototype,
    /** @lends Drupal.TableResponsive# */ {
      /**
       * @param {jQuery.Event} e
       *   The event triggered.
       */
      eventhandlerEvaluateColumnVisibility(e) {
        const pegged = parseInt(this.$link.data('pegged'), 10);
        const hiddenLength = this.$headers.filter(
          '.priority-medium:hidden, .priority-low:hidden',
        ).length;
        // If the table has hidden columns, associate an action link with the
        // table to show the columns.
        if (hiddenLength > 0) {
          this.$link.show().text(this.showText);
        }
        // When the toggle is pegged, its presence is maintained because the user
        // has interacted with it. This is necessary to keep the link visible if
        // the user adjusts screen size and changes the visibility of columns.
        if (!pegged && hiddenLength === 0) {
          this.$link.hide().text(this.hideText);
        }
      },

      /**
       * Toggle the visibility of columns based on their priority.
       *
       * Columns are classed with either 'priority-low' or 'priority-medium'.
       *
       * @param {jQuery.Event} e
       *   The event triggered.
       */
      eventhandlerToggleColumns(e) {
        e.preventDefault();
        const self = this;
        const $hiddenHeaders = this.$headers.filter(
          '.priority-medium:hidden, .priority-low:hidden',
        );
        this.$revealedCells = this.$revealedCells || $();
        // Reveal hidden columns.
        if ($hiddenHeaders.length > 0) {
          $hiddenHeaders.each(function (index, element) {
            const $header = $(this);
            const position = $header.prevAll('th').length;
            self.$table.find('tbody tr').each(function () {
              const $cells = $(this).find('td').eq(position);
              $cells.show();
              // Keep track of the revealed cells, so they can be hidden later.
              self.$revealedCells = $().add(self.$revealedCells).add($cells);
            });
            $header.show();
            // Keep track of the revealed headers, so they can be hidden later.
            self.$revealedCells = $().add(self.$revealedCells).add($header);
          });
          this.$link.text(this.hideText).data('pegged', 1);
        }
        // Hide revealed columns.
        else {
          this.$revealedCells.hide();
          // Strip the 'display:none' declaration from the style attributes of
          // the table cells that .hide() added.
          this.$revealedCells.each(function (index, element) {
            const $cell = $(this);
            const properties = $cell.attr('style').split(';');
            const newProps = [];
            // The hide method adds display none to the element. The element
            // should be returned to the same state it was in before the columns
            // were revealed, so it is necessary to remove the display none value
            // from the style attribute.
            const match = /^display\s*:\s*none$/;
            for (let i = 0; i < properties.length; i++) {
              const prop = properties[i];
              prop.trim();
              // Find the display:none property and remove it.
              const isDisplayNone = match.exec(prop);
              if (isDisplayNone) {
                continue;
              }
              newProps.push(prop);
            }
            // Return the rest of the style attribute values to the element.
            $cell.attr('style', newProps.join(';'));
          });
          this.$link.text(this.showText).data('pegged', 0);
          // Refresh the toggle link.
          $(window).trigger('resize.tableresponsive');
        }
      },
    },
  );

  // Make the TableResponsive object available in the Drupal namespace.
  Drupal.TableResponsive = TableResponsive;
})(jQuery, Drupal, window);