Skip to main content

jQuery plugin for freezing table headers and columns.

/**
 * RWD Table with freezing head and columns for jQuery
 *
 * https://github.com/yidas/jquery-freeze-table/blob/master/dist/js/freeze-table.js
 *
 * @author  Nick Tsai <myintaer@gmail.com>
 * @version 1.3.0
 * @see     https://github.com/yidas/jquery-freeze-table
 */
(function ($, window) {
  "use strict";

  /**
   * Main object
   *
   * @param {element} element
   * @param {object} options
   */
  var FreezeTable = function (element, options) {
    // Target element initialization
    this.$tableWrapper = $(element).first();

    // Options
    this.options = options || {};
    this.namespace = this.options.namespace || "freeze-table";
    this.callback;
    this.scrollBarHeight;
    this.shadow;
    this.fastMode;
    this.backgroundColor;
    this.scrollable;

    // Caches
    this.$table = this.$tableWrapper.children("table");
    this.$container =
      typeof this.options.container !== "undefined" &&
      this.options.container &&
      $(this.options.container).length
        ? $(this.options.container)
        : $(window);
    this.$headTableWrap;
    this.$columnTableWrap;
    this.$columnHeadTableWrap;
    this.$scrollBarWrap;
    this.fixedNavbarHeight;
    this.isWindowScrollX = false;

    // Static class names for clone wraps
    this.headWrapClass = "clone-head-table-wrap";
    this.columnWrapClass = "clone-column-table-wrap";
    this.columnHeadWrapClass = "clone-column-head-table-wrap";
    this.scrollBarWrapClass = "clone-scroll-bar-wrap";

    this.init();

    return this;
  };

  /**
   * Initialization
   */
  FreezeTable.prototype.init = function () {
    // Element check
    if (!this.$table.length) {
      throw "The element must contain a table dom";
    }

    /**
     * Update Mode
     */
    if (this.options === "update") {
      this.destroy();
      this.options = this.$tableWrapper.data("freeze-table-data");
    } else if (this.options === "resize") {
      this.options = this.$tableWrapper.data("freeze-table-data");
      // Get selected FreezeTable's namespace
      this.namespace = this.options.namespace || this.namespace;
      this.resize();
      // Skip init for better performance usage
      return;
    } else {
      // Save to DOM data
      this.$tableWrapper.data("freeze-table-data", this.options);
    }

    /**
     * Options Setting
     */
    var options = this.options;
    var freezeHead =
      typeof options.freezeHead !== "undefined" ? options.freezeHead : true;
    var freezeColumn =
      typeof options.freezeColumn !== "undefined" ? options.freezeColumn : true;
    var freezeColumnHead =
      typeof options.freezeColumnHead !== "undefined"
        ? options.freezeColumnHead
        : true;
    var scrollBar =
      typeof options.scrollBar !== "undefined" ? options.scrollBar : false;
    var fixedNavbar = options.fixedNavbar || ".navbar-fixed-top";
    var callback = options.callback || null;
    this.namespace = this.options.namespace || this.namespace;
    // Default to get window scroll bar height
    this.scrollBarHeight = $.isNumeric(options.scrollBarHeight)
      ? options.scrollBarHeight
      : window.innerWidth - document.documentElement.clientWidth;
    this.shadow =
      typeof options.shadow !== "undefined" ? options.shadow : false;
    this.fastMode =
      typeof options.fastMode !== "undefined" ? options.fastMode : false;
    this.backgroundColor =
      typeof options.backgroundColor !== "undefined"
        ? options.backgroundColor
        : "white";
    this.scrollable =
      typeof options.scrollable !== "undefined" ? options.scrollable : false;

    // Get navbar height for keeping fixed navbar
    this.fixedNavbarHeight = fixedNavbar
      ? $(fixedNavbar).outerHeight() || 0
      : 0;

    // Check existence
    if (this.isInit()) {
      this.destroy();
    }

    // Release height of the table wrapper
    if (!this.scrollable) {
      this.$tableWrapper
        .css("height", "100%")
        .css("min-height", "100%")
        .css("max-height", "100%");
    }

    /**
     * Building
     */
    // Switch for freezeHead
    if (freezeHead) {
      this.buildHeadTable();
    }
    // Switch for freezeColumn
    if (freezeColumn) {
      this.buildColumnTable();
      // X scroll bar
      this.$tableWrapper.css("overflow-x", "scroll");
    }
    // Switch for freezeColumnHead
    if (freezeColumnHead && freezeHead && freezeColumn) {
      this.buildColumnHeadTable();
    }
    // Switch for scrollBar
    if (scrollBar) {
      this.buildScrollBar();
    }

    // Body scroll-x prevention
    var detectWindowScroll = function () {
      // If body scroll-x is opened, close library to prevent Invalid usage
      if (this.$container.scrollLeft() > 0) {
        // Mark
        this.isWindowScrollX = true;
        // Hide all components
        if (this.$headTableWrap) {
          this.$headTableWrap.css("visibility", "hidden");
        }
        if (this.$columnTableWrap) {
          this.$columnTableWrap.css("visibility", "hidden");
        }
        if (this.$columnHeadTableWrap) {
          this.$columnHeadTableWrap.css("visibility", "hidden");
        }
        if (this.$scrollBarWrap) {
          this.$scrollBarWrap.css("visibility", "hidden");
        }
      } else {
        // Unmark
        this.isWindowScrollX = false;
      }
    }.bind(this);
    // Listener of Body scroll-x prevention
    this.$container.on("scroll." + this.namespace, function () {
      detectWindowScroll();
    });

    // Initialization
    this.resize();

    // Callback
    if (typeof callback === "function") {
      callback();
    }
  };

  /**
   * Freeze thead table
   */
  FreezeTable.prototype.buildHeadTable = function () {
    var that = this;

    // Clone the table as Fixed thead
    var $headTable = this.clone(this.$table);

    // Fast Mode
    if (this.fastMode) {
      var $headTable = this.simplifyHead($headTable);
    }

    var headWrapStyles = this.options.headWrapStyles || null;
    // Wrap the Fixed Column table
    this.$headTableWrap = $('<div class="' + this.headWrapClass + '"></div>')
      .append($headTable)
      .css("position", "fixed")
      .css("overflow", "hidden")
      .css("visibility", "hidden")
      .css("top", 0 + this.fixedNavbarHeight)
      .css("z-index", 2);
    // Shadow option
    if (this.shadow) {
      this.$headTableWrap.css(
        "box-shadow",
        "0px 6px 10px -5px rgba(159, 159, 160, 0.8)"
      );
    }
    // Styles option
    if (headWrapStyles && typeof headWrapStyles === "object") {
      $.each(headWrapStyles, function (key, value) {
        that.$headTableWrap.css(key, value);
      });
    }
    // Add into target table wrap
    this.$tableWrapper.append(this.$headTableWrap);

    /**
     * Listener - Table scroll for effecting Freeze Column
     */
    this.$tableWrapper.on("scroll." + this.namespace, function () {
      // this.$headTableWrap.css('left', this.$table.offset().left);
      that.$headTableWrap.scrollLeft($(this).scrollLeft());
    });

    // Scrollable option
    if (this.scrollable) {
      var handler = function (window, that) {
        var top = that.$tableWrapper.offset().top;

        // Detect Current container's top is in the table scope
        if (
          that.$tableWrapper.scrollTop() > 0 &&
          top > that.fixedNavbarHeight
        ) {
          that.$headTableWrap.offset({ top: top });
          that.$headTableWrap.css("visibility", "visible");
        } else {
          that.$headTableWrap.css("visibility", "hidden");
        }
      };

      /**
       * Listener - Window scroll for effecting freeze head table
       */
      this.$tableWrapper.on("scroll." + this.namespace, function () {
        handler(window, that);
      });

      this.$container.on("scroll." + this.namespace, function () {
        handler(window, that);
      });
    }
    // Default with window container
    else if ($.isWindow(that.$container.get(0))) {
      /**
       * Listener - Window scroll for effecting freeze head table
       */
      this.$container.on("scroll." + this.namespace, function () {
        // Current container's top position
        var topPosition = that.$container.scrollTop() + that.fixedNavbarHeight;
        var tableTop = that.$table.offset().top - 1;

        // Detect Current container's top is in the table scope
        if (
          tableTop - 1 <= topPosition &&
          tableTop + that.$table.outerHeight() - 1 >= topPosition
        ) {
          that.$headTableWrap.css("visibility", "visible");
        } else {
          that.$headTableWrap.css("visibility", "hidden");
        }
      });
    }
    // Container setting
    else {
      /**
       * Listener - Window scroll for effecting freeze head table
       */
      this.$container.on("scroll." + this.namespace, function () {
        var windowTop = $(window).scrollTop();
        var tableTop = that.$table.offset().top - 1;

        // Detect Current container's top is in the table scope
        if (
          tableTop <= windowTop &&
          tableTop + that.$table.outerHeight() - 1 >= windowTop
        ) {
          that.$headTableWrap.offset({ top: windowTop });
          that.$headTableWrap.css("visibility", "visible");
        } else {
          that.$headTableWrap.css("visibility", "hidden");
        }
      });
    }

    /**
     * Listener - Window resize for effecting freeze head table
     */
    this.$container.on("resize." + this.namespace, function () {
      // Scrollable check and prevention
      var headTableWrapWidth = that.scrollable
        ? that.$tableWrapper.width() - that.scrollBarHeight
        : that.$tableWrapper.width();
      headTableWrapWidth =
        headTableWrapWidth > 0
          ? headTableWrapWidth
          : that.$tableWrapper.width();
      that.$headTableWrap.css("width", headTableWrapWidth);
      that.$headTableWrap.css(
        "height",
        that.$table.find("thead").outerHeight()
      );
    });
  };

  /**
   * Freeze column table
   */
  FreezeTable.prototype.buildColumnTable = function () {
    var that = this;

    /**
     * Setting
     */
    var columnWrapStyles = this.options.columnWrapStyles || null;
    var columnNum = this.options.columnNum || 1;
    var columnKeep =
      typeof this.options.columnKeep !== "undefined"
        ? this.options.columnKeep
        : false;
    // Shadow option
    var defaultColumnBorderWidth = this.shadow ? 0 : 1;
    var columnBorderWidth =
      typeof this.options.columnBorderWidth !== "undefined"
        ? this.options.columnBorderWidth
        : defaultColumnBorderWidth;

    // Clone the table as Fixed Column table
    var $columnTable = this.clone(this.$table);

    // Wrap the Fixed Column table
    this.$columnTableWrap = $(
      '<div class="' + this.columnWrapClass + '"></div>'
    )
      .append($columnTable)
      .css("position", "fixed")
      .css("overflow", "hidden")
      .css("visibility", "hidden")
      .css("z-index", 1);
    // Shadow option
    if (this.shadow) {
      this.$columnTableWrap.css(
        "box-shadow",
        "6px 0px 10px -5px rgba(159, 159, 160, 0.8)"
      );
    }
    // Styles option
    if (columnWrapStyles && typeof columnWrapStyles === "object") {
      $.each(columnWrapStyles, function (key, value) {
        that.$columnTableWrap.css(key, value);
      });
    }
    // Scrollable
    if (this.scrollable) {
      // Scrollable check and prevention
      var columnTableWrapHeight =
        this.$tableWrapper.height() - this.scrollBarHeight;
      columnTableWrapHeight =
        columnTableWrapHeight > 0
          ? columnTableWrapHeight
          : this.$tableWrapper.height();
      this.$columnTableWrap.height(columnTableWrapHeight);
    }
    // Add into target table wrap
    this.$tableWrapper.append(this.$columnTableWrap);

    /**
     * localize the column wrap to current top
     */
    var localizeWrap = function () {
      that.$columnTableWrap.offset({ top: that.$tableWrapper.offset().top });
    };

    // Column keep option
    if (columnKeep) {
      this.$columnTableWrap.css("visibility", "visible");
    } else {
      // Scrollable option
      if (that.scrollable) {
        /**
         * Listener - Table scroll for effecting Freeze Column
         */
        this.$tableWrapper.on("scroll." + this.namespace, function () {
          // Detect for horizontal scroll
          if ($(this).scrollLeft() > 0) {
            // Scrollable localization
            that.$columnTableWrap.scrollTop(that.$tableWrapper.scrollTop());
            that.$columnTableWrap.css("visibility", "visible");
          } else {
            that.$columnTableWrap.css("visibility", "hidden");
          }
        });
      } else {
        /**
         * Listener - Table scroll for effecting Freeze Column
         */
        this.$tableWrapper.on("scroll." + this.namespace, function () {
          // Disable while isWindowScrollX
          if (that.isWindowScrollX) return;

          // Detect for horizontal scroll
          if ($(this).scrollLeft() > 0) {
            that.$columnTableWrap.css("visibility", "visible");
          } else {
            that.$columnTableWrap.css("visibility", "hidden");
          }
        });
      }
    }

    /**
     * Listener - Window resize for effecting tables
     */
    this.$container.on("resize." + this.namespace, function () {
      // Follows origin table's width
      $columnTable.width(that.$table.width());

      /**
       * Dynamic column calculation
       */
      // Get width by fixed column with number setting
      var width = 0 + columnBorderWidth;
      for (var i = 1; i <= columnNum; i++) {
        // th/td detection
        var th = that.$table.find("th:nth-child(" + i + ")").outerWidth();
        var addWidth =
          th > 0
            ? th
            : that.$table.find("td:nth-child(" + i + ")").outerWidth();
        width += addWidth;
      }
      that.$columnTableWrap.width(width);

      localizeWrap();
    });

    /**
     * Listener - Window scroll for effecting freeze column table
     */
    this.$container.on("scroll." + this.namespace, function () {
      localizeWrap();
    });
  };

  /**
   * Freeze column thead table
   */
  FreezeTable.prototype.buildColumnHeadTable = function () {
    var that = this;

    // Clone head table wrap
    this.$columnHeadTableWrap = this.clone(this.$headTableWrap);

    // Fast Mode
    if (this.fastMode) {
      this.$columnHeadTableWrap = this.simplifyHead(this.$columnHeadTableWrap);
    }

    var columnHeadWrapStyles = this.options.columnHeadWrapStyles || null;

    this.$columnHeadTableWrap
      .removeClass(this.namespace)
      .addClass(this.columnHeadWrapClass)
      .css("z-index", 3);
    // Shadow option
    if (this.shadow) {
      this.$columnHeadTableWrap.css("box-shadow", "none");
    }
    // Styles option
    if (columnHeadWrapStyles && typeof columnHeadWrapStyles === "object") {
      $.each(columnHeadWrapStyles, function (key, value) {
        this.$columnHeadTableWrap.css(key, value);
      });
    }

    // Add into target table wrap
    this.$tableWrapper.append(this.$columnHeadTableWrap);

    // Scrollable option
    if (this.scrollable) {
      var detect = function () {
        var top = that.$tableWrapper.offset().top;

        // Detect Current container's top is in the table scope
        if (
          that.$tableWrapper.scrollTop() > 0 &&
          top > that.fixedNavbarHeight
        ) {
          that.$columnHeadTableWrap.offset({ top: top });
          that.$columnHeadTableWrap.css("visibility", "visible");
        } else {
          that.$columnHeadTableWrap.css("visibility", "hidden");
        }
      };

      /**
       * Listener - Window scroll for effecting freeze head table
       */
      $(this.$tableWrapper).on("scroll." + this.namespace, function () {
        detect();
      });
    }
    // Default with window container
    else if ($.isWindow(this.$container.get(0))) {
      var detect = function () {
        // Current container's top position
        var topPosition = that.$container.scrollTop() + that.fixedNavbarHeight;
        var tableTop = that.$table.offset().top - 1;

        // Detect Current container's top is in the table scope
        if (
          tableTop - 1 <= topPosition &&
          tableTop + that.$table.outerHeight() - 1 >= topPosition &&
          that.$tableWrapper.scrollLeft() > 0
        ) {
          that.$columnHeadTableWrap.css("visibility", "visible");
        } else {
          that.$columnHeadTableWrap.css("visibility", "hidden");
        }
      };
    }
    // Container setting
    else {
      var detect = function () {
        var windowTop = $(window).scrollTop();
        var tableTop = that.$table.offset().top - 1;

        // Detect Current container's top is in the table scope
        if (
          tableTop <= windowTop &&
          tableTop + that.$table.outerHeight() - 1 >= windowTop &&
          that.$tableWrapper.scrollLeft() > 0
        ) {
          that.$columnHeadTableWrap.offset({ top: windowTop });
          that.$columnHeadTableWrap.css("visibility", "visible");
        } else {
          that.$columnHeadTableWrap.css("visibility", "hidden");
        }
      };
    }

    /**
     * Listener - Window scroll for effecting Freeze column-head table
     */
    this.$container.on("scroll." + this.namespace, function () {
      detect();
    });

    /**
     * Listener - Table scroll for effecting Freeze column-head table
     */
    this.$tableWrapper.on("scroll." + this.namespace, function () {
      // Disable while isWindowScrollX
      if (that.isWindowScrollX) return;

      detect();
    });

    /**
     * Listener - Window resize for effecting freeze column-head table
     */
    this.$container.on("resize." + this.namespace, function () {
      // Table synchronism
      that.$columnHeadTableWrap
        .find("> table")
        .css("width", that.$table.width());
      that.$columnHeadTableWrap.css("width", that.$columnTableWrap.width());
      that.$columnHeadTableWrap.css(
        "height",
        that.$table.find("thead").outerHeight()
      );
    });
  };

  /**
   * Freeze scroll bar
   */
  FreezeTable.prototype.buildScrollBar = function () {
    var that = this;

    var theadHeight = this.$table.find("thead").outerHeight();

    // Scroll wrap container
    var $scrollBarContainer = $(
      '<div class="' + this.scrollBarWrapClass + '"></div>'
    )
      .css("width", this.$table.width())
      .css("height", 1);

    // Wrap the Fixed Column table
    this.$scrollBarWrap = $(
      '<div class="' + this.scrollBarWrapClass + '"></div>'
    )
      .css("position", "fixed")
      .css("overflow-x", "scroll")
      .css("visibility", "hidden")
      .css("bottom", 0)
      .css("z-index", 2)
      .css("width", this.$tableWrapper.width())
      .css("height", this.scrollBarHeight);

    // Add into target table wrap
    this.$scrollBarWrap.append($scrollBarContainer);
    this.$tableWrapper.append(this.$scrollBarWrap);

    /**
     * Listener - Freeze scroll bar effected Table
     */
    this.$scrollBarWrap.on("scroll." + this.namespace, function () {
      that.$tableWrapper.scrollLeft($(this).scrollLeft());
    });

    /**
     * Listener - Table scroll for effecting Freeze scroll bar
     */
    this.$tableWrapper.on("scroll." + this.namespace, function () {
      // this.$headTableWrap.css('left', $table.offset().left);
      that.$scrollBarWrap.scrollLeft($(this).scrollLeft());
    });

    /**
     * Listener - Window scroll for effecting scroll bar
     */
    this.$container.on("scroll." + this.namespace, function () {
      // Current container's top position
      var bottomPosition =
        that.$container.scrollTop() +
        that.$container.height() -
        theadHeight +
        that.fixedNavbarHeight;

      // Detect Current container's top is in the table scope
      if (
        that.$table.offset().top - 1 <= bottomPosition &&
        that.$table.offset().top + that.$table.outerHeight() - 1 >=
          bottomPosition
      ) {
        that.$scrollBarWrap.css("visibility", "visible");
      } else {
        that.$scrollBarWrap.css("visibility", "hidden");
      }
    });

    /**
     * Listener - Window resize for effecting scroll bar
     */
    this.$container.on("resize." + this.namespace, function () {
      // Update width
      $scrollBarContainer.css("width", that.$table.width());
      // Update Wrap
      that.$scrollBarWrap.css("width", that.$tableWrapper.width());
    });
  };

  /**
   * Clone element
   *
   * @param {element} element
   */
  FreezeTable.prototype.clone = function (element) {
    var $clone = $(element).clone().removeAttr("id"); // Remove ID

    // Bootstrap background-color transparent problem
    if (this.backgroundColor) {
      $clone.css("background-color", this.backgroundColor);
    }

    return $clone;
  };

  /**
   * simplify cloned head table
   *
   * @param {element} table Table element
   */
  FreezeTable.prototype.simplifyHead = function (table) {
    var that = this;

    var $headTable = $(table);
    // Remove non-display DOM but keeping first row for accuracy
    $headTable.find("> tr, > tbody > tr, tfoot > tr").not(":first").remove();
    // Each th/td width synchronism
    $.each(
      $headTable.find("> thead > tr:nth-child(1) >"),
      function (key, value) {
        var width = that.$table
          .find(
            "> thead > tr:nth-child(1) > :nth-child(" + parseInt(key + 1) + ")"
          )
          .outerWidth();
        $(this).css("width", width);
      }
    );

    return $headTable;
  };

  /**
   * Detect is already initialized
   */
  FreezeTable.prototype.isInit = function () {
    // Check existence DOM
    if (this.$tableWrapper.find("." + this.headWrapClass).length) return true;
    if (this.$tableWrapper.find("." + this.columnWrapClass).length) return true;
    if (this.$tableWrapper.find("." + this.columnHeadWrapClass).length)
      return true;
    if (this.$tableWrapper.find("." + this.scrollBarWrapClass).length)
      return true;

    return false;
  };

  /**
   * Unbind all events by same namespace
   */
  FreezeTable.prototype.unbind = function () {
    this.$container.off("resize." + this.namespace);
    this.$container.off("scroll." + this.namespace);
    this.$tableWrapper.off("scroll." + this.namespace);
  };

  /**
   * Destroy Freeze Table by same namespace
   */
  FreezeTable.prototype.destroy = function () {
    this.unbind();
    this.$tableWrapper.find("." + this.headWrapClass).remove();
    this.$tableWrapper.find("." + this.columnWrapClass).remove();
    this.$tableWrapper.find("." + this.columnHeadWrapClass).remove();
    this.$tableWrapper.find("." + this.scrollBarWrapClass).remove();
  };

  /**
   * Resize trigger for current same namespace
   */
  FreezeTable.prototype.resize = function () {
    this.$container.trigger("resize." + this.namespace);
    this.$container.trigger("scroll." + this.namespace);
    this.$tableWrapper.trigger("scroll." + this.namespace);

    return true;
  };

  /**
   * Update for Dynamic Content
   */
  FreezeTable.prototype.update = function () {
    // Same as re-new object
    this.options = "update";
    this.init();
    return this;
  };

  /**
   * Interface
   */
  // Class for single element
  window.FreezeTable = FreezeTable;
  // jQuery interface
  $.fn.freezeTable = function (options) {
    // Single/Multiple mode
    if (this.length === 1) {
      return new FreezeTable(this, options);
    } else if (this.length > 1) {
      var result = [];
      // Multiple elements bundle
      this.each(function () {
        result.push(new FreezeTable(this, options));
      });

      return result;
    }

    return false;
  };
})(jQuery, window);