Security update for Core, with self-updated composer
[yaffs-website] / web / core / misc / tableheader.es6.js
diff --git a/web/core/misc/tableheader.es6.js b/web/core/misc/tableheader.es6.js
new file mode 100644 (file)
index 0000000..a5aa663
--- /dev/null
@@ -0,0 +1,312 @@
+/**
+ * @file
+ * Sticky table headers.
+ */
+
+(function ($, Drupal, displace) {
+  /**
+   * 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];
+  }
+
+  // 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');
+  }
+
+  // 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);
+    }
+  }
+
+  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,
+  });
+
+  /**
+   * 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();
+  }
+
+  /**
+   * 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.parent.Drupal.displace));