text: /**
   * @file
   * Sticky table headers.
   */
  
  (function($, Drupal, displace) {
    /**
     * Constructor for the tableHeader object. Provides sticky table headers.
     *
     * TableHeader will make the current table header stick to the top of the page
     * if the table is very long.
     *
     * @constructor Drupal.TableHeader
     *
     * @param {HTMLElement} table
     *   DOM object for the table to add a sticky header to.
     *
     * @listens event:columnschange
     */
    function TableHeader(table) {
      const $table = $(table);
  
      /**
       * @name Drupal.TableHeader#$originalTable
       *
       * @type {HTMLElement}
       */
      this.$originalTable = $table;
  
      /**
       * @type {jQuery}
       */
      this.$originalHeader = $table.children('thead');
  
      /**
       * @type {jQuery}
       */
      this.$originalHeaderCells = this.$originalHeader.find('> tr > th');
  
      /**
       * @type {null|bool}
       */
      this.displayWeight = null;
      this.$originalTable.addClass('sticky-table');
      this.tableHeight = $table[0].clientHeight;
      this.tableOffset = this.$originalTable.offset();
  
      // React to columns change to avoid making checks in the scroll callback.
      this.$originalTable.on(
        'columnschange',
        { tableHeader: this },
        (e, display) => {
          const tableHeader = e.data.tableHeader;
          if (
            tableHeader.displayWeight === null ||
            tableHeader.displayWeight !== display
          ) {
            tableHeader.recalculateSticky();
          }
          tableHeader.displayWeight = display;
        },
      );
  
      // Create and display sticky header.
      this.createSticky();
    }
  
    // Helper method to loop through tables and execute a method.
    function forTables(method, arg) {
      const tables = TableHeader.tables;
      const il = tables.length;
      for (let i = 0; i < il; i++) {
        tables[i][method](arg);
      }
    }
  
    // Select and initialize sticky table headers.
    function tableHeaderInitHandler(e) {
      const $tables = $(e.data.context)
        .find('table.sticky-enabled')
        .once('tableheader');
      const il = $tables.length;
      for (let i = 0; i < il; i++) {
        TableHeader.tables.push(new TableHeader($tables[i]));
      }
      forTables('onScroll');
    }
  
    /**
     * Attaches sticky table headers.
     *
     * @type {Drupal~behavior}
     *
     * @prop {Drupal~behaviorAttach} attach
     *   Attaches the sticky table header behavior.
     */
    Drupal.behaviors.tableHeader = {
      attach(context) {
        $(window).one(
          'scroll.TableHeaderInit',
          { context },
          tableHeaderInitHandler,
        );
      },
    };
  
    function scrollValue(position) {
      return document.documentElement[position] || document.body[position];
    }
  
    function tableHeaderResizeHandler(e) {
      forTables('recalculateSticky');
    }
  
    function tableHeaderOnScrollHandler(e) {
      forTables('onScroll');
    }
  
    function tableHeaderOffsetChangeHandler(e, offsets) {
      forTables('stickyPosition', offsets.top);
    }
  
    // Bind event that need to change all tables.
    $(window).on({
      /**
       * When resizing table width can change, recalculate everything.
       *
       * @ignore
       */
      'resize.TableHeader': tableHeaderResizeHandler,
  
      /**
       * Bind only one event to take care of calling all scroll callbacks.
       *
       * @ignore
       */
      'scroll.TableHeader': tableHeaderOnScrollHandler,
    });
    // Bind to custom Drupal events.
    $(document).on({
      /**
       * Recalculate columns width when window is resized and when show/hide
       * weight is triggered.
       *
       * @ignore
       */
      'columnschange.TableHeader': tableHeaderResizeHandler,
  
      /**
       * Recalculate TableHeader.topOffset when viewport is resized.
       *
       * @ignore
       */
      'drupalViewportOffsetChange.TableHeader': tableHeaderOffsetChangeHandler,
    });
  
    /**
     * Store the state of TableHeader.
     */
    $.extend(
      TableHeader,
      /** @lends Drupal.TableHeader */ {
        /**
         * This will store the state of all processed tables.
         *
         * @type {Array.<Drupal.TableHeader>}
         */
        tables: [],
      },
    );
  
    /**
     * Extend TableHeader prototype.
     */
    $.extend(
      TableHeader.prototype,
      /** @lends Drupal.TableHeader# */ {
        /**
         * Minimum height in pixels for the table to have a sticky header.
         *
         * @type {number}
         */
        minHeight: 100,
  
        /**
         * Absolute position of the table on the page.
         *
         * @type {?Drupal~displaceOffset}
         */
        tableOffset: null,
  
        /**
         * Absolute position of the table on the page.
         *
         * @type {?number}
         */
        tableHeight: null,
  
        /**
         * Boolean storing the sticky header visibility state.
         *
         * @type {bool}
         */
        stickyVisible: false,
  
        /**
         * Create the duplicate header.
         */
        createSticky() {
          // Clone the table header so it inherits original jQuery properties.
          const $stickyHeader = this.$originalHeader.clone(true);
          // Hide the table to avoid a flash of the header clone upon page load.
          this.$stickyTable = $('<table class="sticky-header"/>')
            .css({
              visibility: 'hidden',
              position: 'fixed',
              top: '0px',
            })
            .append($stickyHeader)
            .insertBefore(this.$originalTable);
  
          this.$stickyHeaderCells = $stickyHeader.find('> tr > th');
  
          // Initialize all computations.
          this.recalculateSticky();
        },
  
        /**
         * Set absolute position of sticky.
         *
         * @param {number} offsetTop
         *   The top offset for the sticky header.
         * @param {number} offsetLeft
         *   The left offset for the sticky header.
         *
         * @return {jQuery}
         *   The sticky table as a jQuery collection.
         */
        stickyPosition(offsetTop, offsetLeft) {
          const css = {};
          if (typeof offsetTop === 'number') {
            css.top = `${offsetTop}px`;
          }
          if (typeof offsetLeft === 'number') {
            css.left = `${this.tableOffset.left - offsetLeft}px`;
          }
          return this.$stickyTable.css(css);
        },
  
        /**
         * Returns true if sticky is currently visible.
         *
         * @return {bool}
         *   The visibility status.
         */
        checkStickyVisible() {
          const scrollTop = scrollValue('scrollTop');
          const tableTop = this.tableOffset.top - displace.offsets.top;
          const tableBottom = tableTop + this.tableHeight;
          let visible = false;
  
          if (tableTop < scrollTop && scrollTop < tableBottom - this.minHeight) {
            visible = true;
          }
  
          this.stickyVisible = visible;
          return visible;
        },
  
        /**
         * Check if sticky header should be displayed.
         *
         * This function is throttled to once every 250ms to avoid unnecessary
         * calls.
         *
         * @param {jQuery.Event} e
         *   The scroll event.
         */
        onScroll(e) {
          this.checkStickyVisible();
          // Track horizontal positioning relative to the viewport.
          this.stickyPosition(null, scrollValue('scrollLeft'));
          this.$stickyTable.css(
            'visibility',
            this.stickyVisible ? 'visible' : 'hidden',
          );
        },
  
        /**
         * Event handler: recalculates position of the sticky table header.
         *
         * @param {jQuery.Event} event
         *   Event being triggered.
         */
        recalculateSticky(event) {
          // Update table size.
          this.tableHeight = this.$originalTable[0].clientHeight;
  
          // Update offset top.
          displace.offsets.top = displace.calculateOffset('top');
          this.tableOffset = this.$originalTable.offset();
          this.stickyPosition(displace.offsets.top, scrollValue('scrollLeft'));
  
          // Update columns width.
          let $that = null;
          let $stickyCell = null;
          let display = null;
          // Resize header and its cell widths.
          // Only apply width to visible table cells. This prevents the header from
          // displaying incorrectly when the sticky header is no longer visible.
          const il = this.$originalHeaderCells.length;
          for (let i = 0; i < il; i++) {
            $that = $(this.$originalHeaderCells[i]);
            $stickyCell = this.$stickyHeaderCells.eq($that.index());
            display = $that.css('display');
            if (display !== 'none') {
              $stickyCell.css({ width: $that.css('width'), display });
            } else {
              $stickyCell.css('display', 'none');
            }
          }
          this.$stickyTable.css('width', this.$originalTable.outerWidth());
        },
      },
    );
  
    // Expose constructor in the public space.
    Drupal.TableHeader = TableHeader;
  })(jQuery, Drupal, window.Drupal.displace);
  