diff --git a/scrollbalance.js b/scrollbalance.js
index f221fff..34721f5 100644
--- a/scrollbalance.js
+++ b/scrollbalance.js
@@ -10,312 +10,292 @@
*
*/
-(function (window, factory) {
- // universal module definition - thanks @desandro
- /* eslint-disable eqeqeq, no-undef */
- if (typeof define == 'function' && define.amd) {
- // AMD
- define(['jquery'], factory);
- } else if (typeof module == 'object' && module.exports) {
- // CommonJS
- module.exports = factory(require('jquery'));
- } else {
- // browser global
- window.ScrollBalance = factory(window.jQuery);
- }
- /* eslint-enable eqeqeq, no-undef */
-}(window, function factory ($) {
- 'use strict';
+var INNER_CLASSNAME = 'scrollbalance-inner'
- var INNER_CLASSNAME = 'scrollbalance-inner';
- function ScrollBalance (columns, options) {
- this.columns = columns;
- this.columnData = [];
- this.settings = $.extend({
+export default class ScrollBalance {
+ constructor (columns, options) {
+ this.columns = columns
+ this.columnData = []
+ this.settings = Object.assign({
// threshold for activating the plugin, eg the column heights must
// differ by at least this amount to be affected.
threshold: 100,
-
// disable the plugin if the screen width is less than this
minwidth: null
- }, options);
-
- this.balance_enabled = true;
- this.scrollTop = 0;
- this.setup();
+ }, options || {})
+ this.balance_enabled = true
+ this.scrollTop = 0
+ this.setup()
}
- ScrollBalance.prototype = {
- // "PUBLIC" METHODS:
- initialize: function () {
- /* Position each column inner absolutely within the column,
- and set the column heights, since their content is now
- positioned absolutely.
- Should be called whenever column content changes, or window
- is resized. */
-
- var that = this;
-
- function columnHeight (col) {
- var inner = col.find('.' + INNER_CLASSNAME);
- return inner.height() +
- parseInt(col.css('borderTop') || 0) +
- parseInt(col.css('paddingTop') || 0) +
- parseInt(col.css('paddingBottom') || 0) +
- parseInt(col.css('borderBottom') || 0);
- }
-
- // Calculate the maximum column height, i.e. how high the container
- // should be (don't assume the user is using a clearfix hack on their
- // container), and the container offset. If there's only one column, use
- // the parent for both calculations
- if (this.columns.length === 1) {
- this.containerHeight = this.columns.parent().height();
- this.containerTop = this.columns.parent().offset().top;
- } else {
- var height = 0;
- this.columns.each(function () {
- height = Math.max(height, columnHeight($(this)));
- });
- this.containerHeight = height;
- this.containerTop = this.columns.eq(0).offset().top;
- }
-
- this.columns.each(function (i) {
- var col = $(this);
- var inner = col.find('.' + INNER_CLASSNAME);
-
- // save column data to avoid hitting the DOM on scroll
- if (!that.columnData[i]) {
- that.columnData[i] = {
- // default initial values here
- fixTop: 0
- };
- }
- var columnData = that.columnData[i];
-
- // calculate actual height regardless of what it's previously been
- // set to
- columnData.height = columnHeight(col);
+ getOffset (el) {
+ /* helper, returns elment offset */
+ el = el.getBoundingClientRect()
+ return {
+ left: el.left + window.scrollX,
+ top: el.top + window.scrollY
+ }
+ }
- // disable if not enough difference in height between container and
- // column
- columnData.enabled = (that.containerHeight - columnData.height) >
- that.settings.threshold;
+ windowSize () {
+ /* helper, returns window dimentions */
+ var e = window
+ var a = 'inner'
+ if (!('innerWidth' in window)) {
+ a = 'client'
+ e = document.documentElement || document.body
+ }
+ return {width: e[a + 'Width'], height: e[a + 'Height']}
+ }
- columnData.fixLeft = col.offset().left +
- (parseInt(col.css('borderLeftWidth'), 10) || 0);
+ windowScroll () {
+ /* helper, returns current window scroll position */
+ var doc = document.documentElement
+ return {
+ left: (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0),
+ top: (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0)
+ }
+ }
+ // "PUBLIC" METHODS:
+ initialize () {
+ /* Position each column inner absolutely within the column,
+ and set the column heights, since their content is now
+ positioned absolutely.
+ Should be called whenever column content changes, or window
+ is resized. */
+
+ function columnHeight (col) {
+ // TODO border included?
+ var inner = col.querySelector('.' + INNER_CLASSNAME)
+ var styles = window.getComputedStyle(inner)
+ var margin = parseFloat(styles.marginTop) +
+ parseFloat(styles.marginBottom)
+ return Math.ceil(inner.offsetHeight + margin)
+ }
- columnData.minFixTop = Math.min(0, that.winHeight - columnData.height);
- columnData.maxFixTop = 0;
+ // Calculate the maximum column height, i.e. how high the container
+ // should be (don't assume the user is using a clearfix hack on their
+ // container), and the container offset. If there's only one column, use
+ // the parent for both calculations
+ if (this.columns.length === 1) {
+ this.containerHeight = this.columns[0].parentElement.offsetHeight
+ this.containerTop = this.getOffset(this.columns[0].parentElement).top
+ } else {
+ var height = 0
+ this.columns.forEach((column) => {
+ height = Math.max(height, columnHeight(column))
+ })
+ this.containerHeight = height
+ this.containerTop = this.getOffset(this.columns[0]).top
+ }
- // uncomment this to have it stick to the bottom too rather than just
- // the top
- // columnData.maxFixTop = Math.max(
- // 0, that.winHeight - columnData.height);
+ this.columns.forEach((column, i) => {
+ var inner = column.querySelector('.' + INNER_CLASSNAME)
+ var columnStyles = window.getComputedStyle(column)
- if (that.balance_enabled && columnData.enabled) {
- inner.css({
- width: col.css('width'),
- transform: 'translateZ(0px)',
- padding: col.css('padding')
- // other css for this element is handled in balance()
- });
- col.css({
- height: inner.css('height')
- });
- } else {
- // reset state
- inner.css({
- width: '',
- transform: '',
- padding: ''
- });
- col.height('');
+ // save column data to avoid hitting the DOM on scroll
+ if (!this.columnData[i]) {
+ this.columnData[i] = {
+ // default initial values here
+ fixTop: 0
}
- that.balance(col, columnData, true);
- });
- },
- resize: function (winWidth, winHeight) {
- this.winHeight = winHeight;
-
- if (this.settings.minwidth !== null) {
- this.balance_enabled = (winWidth >= this.settings.minwidth);
}
- this.initialize();
- },
- scroll: function (scrollTop, scrollLeft) {
- var scrollDelta = scrollTop - this.scrollTop;
- this.scrollTop = scrollTop;
- this.scrollLeft = scrollLeft;
- this.balance_all(false, scrollDelta);
- },
- bind: function () {
- /* Bind scrollbalance handlers to the scroll and resize events */
- var that = this;
- $(window).on('resize.scrollbalance', function () {
- that.resize($(window).width(), $(window).height());
- });
- $(window).on('scroll.scrollbalance', function () {
- that.scroll($(window).scrollTop(), $(window).scrollLeft());
- });
- $(window).trigger('resize');
- $(window).trigger('scroll');
- },
- unbind: function () {
- /* Unbind all scrollbalance handlers. */
- $(window).off('resize.scrollbalance');
- $(window).off('scroll.scrollbalance');
- },
- disable: function () {
- /* Temporarily disable scrollbalance */
- this.balance_enabled = false;
- this.initialize();
- },
- enable: function () {
- /* Re-enable scrollbalance */
- this.balance_enabled = true;
- this.initialize();
- },
- teardown: function () {
- /* Remove all traces of scrollbalance from the content */
-
- this.columns.each(function () {
- var col = $(this);
- var inner = col.find('.' + INNER_CLASSNAME);
-
- if (inner.data('sb-created')) {
- inner.children().appendTo(col);
- inner.remove();
- }
- col.css({
- position: '',
- height: ''
- });
- });
- },
-
- // "PRIVATE" METHODS:
- setup: function () {
- /* Append an "inner" element to each column, if it isn't already there,
- and move the column's content into this element, so that the
- content's vertical position can be controlled independently of the
- column's (usually floated) position.
- Should only be called once, on setup. */
-
- this.columns.each(function () {
- var col = $(this);
- var inner = col.find('.' + INNER_CLASSNAME);
-
- if (col.css('position') === 'static') {
- col.css('position', 'relative');
- }
-
- if (!inner.length) {
- inner = $('
').addClass(INNER_CLASSNAME)
- .append(col.children())
- .data('sb-created', true);
- col.html('').append(inner);
- }
- });
- },
- balance: function (col, columnData, force, scrollDelta) {
- /* Using the scroll position, container offset, and column
- height, determine whether the column should be fixed or
- absolute, and position it accordingly. */
+ var columnData = this.columnData[i]
+
+ // calculate actual height regardless of what it's previously been
+ // set to
+ columnData.height = columnHeight(column)
+
+ // disable if not enough difference in height between container and
+ // column
+ columnData.enabled = (this.containerHeight - columnData.height) >
+ this.settings.threshold
+ // TODO border included
+ columnData.fixLeft = this.getOffset(column).left //+
+ // (parseInt(col.css('borderLeftWidth'), 10) || 0)
+
+ columnData.minFixTop = Math.min(0, this.winHeight - columnData.height)
+ columnData.maxFixTop = 0
+
+ // uncomment this to have it stick to the bottom too rather than just
+ // the top
+ // columnData.maxFixTop = Math.max(
+ // 0, this.winHeight - columnData.height)
+
+ if (this.balance_enabled && columnData.enabled) {
+ // other css for this element is handled in balance()
+ inner.style.width = columnStyles.width
+ inner.style.padding = columnStyles.padding
+ // inner.style.transform = 'translateZ(0px)'
+ column.style.height = columnStyles.height
+ } else {
+ // reset state
+ inner.style.width = null
+ inner.style.padding = null
+ // inner.style.transform = null
+ column.style.height = null
+ }
+ this.balance(column, columnData, true)
+ })
+ }
+ resize (winWidth, winHeight) {
+ this.winHeight = winHeight
- var state;
- var fixTop = columnData.fixTop;
+ if (this.settings.minwidth !== null) {
+ this.balance_enabled = (winWidth >= this.settings.minwidth)
+ }
+ this.initialize()
+ }
+ scroll (scrollTop, scrollLeft) {
+ var scrollDelta = scrollTop - this.scrollTop
+ this.scrollTop = scrollTop
+ this.scrollLeft = scrollLeft
+ this.balanceAll(false, scrollDelta)
+ }
+ bind () {
+ let resizeTimer
+ /* Bind scrollbalance handlers to the scroll and resize events */
+ window.addEventListener('resize', (e) => {
+ // debounced
+ clearTimeout(resizeTimer)
+ resizeTimer = window.setTimeout(() => {
+ const wSize = this.windowSize()
+ this.resize(wSize.width, wSize.height)
+ }, 250)
+ })
+ window.addEventListener('scroll', (e) => {
+ const wScroll = this.windowScroll()
+ this.scroll(wScroll.top, wScroll.left)
+ })
+ // init call
+ const wSize = this.windowSize()
+ const wScroll = this.windowScroll()
+ this.resize(wSize.width, wSize.height)
+ this.scroll(wScroll.top, wScroll.left)
+ }
+ unbind () {
+ /* Unbind all scrollbalance handlers. */
+ window.removeEventListener('resize', this.resize)
+ window.removeEventListener('scroll', this.scroll)
+ }
+ disable () {
+ /* Temporarily disable scrollbalance */
+ this.balance_enabled = false
+ this.initialize()
+ }
+ enable () {
+ /* Re-enable scrollbalance */
+ this.balance_enabled = true
+ this.initialize()
+ }
+ teardown () {
+ /* Remove all traces of scrollbalance from the content */
+ this.columns.forEach((column) => {
+ var inner = column.querySelector('.' + INNER_CLASSNAME)
+
+ if (inner.setAttribute('data-sb-created')) {
+ // TODO check this moves content
+ column.parentElement.append(inner.children)
+ }
+ column.style.position = null
+ column.style.height = null
+ })
+ }
- if (scrollDelta === undefined) {
- scrollDelta = 0;
+ // "PRIVATE" METHODS:
+ setup () {
+ /* Append an "inner" element to each column, if it isn't already there,
+ and move the column's content into this element, so that the
+ content's vertical position can be controlled independently of the
+ column's (usually floated) position.
+ Should only be called once, on setup. */
+
+ this.columns.forEach((column) => {
+ var inner = column.querySelector('.' + INNER_CLASSNAME)
+ var columnStyles = window.getComputedStyle(column)
+ if (columnStyles.position === 'static') {
+ column.style.position = 'relative'
}
- if (!columnData.enabled || !this.balance_enabled) {
- state = 'disabled';
- } else {
- // determine state, one of
- // - top
- // - bottom
- // - fixed
+ if (!inner) {
+ inner = document.createElement('div')
+ inner.className = INNER_CLASSNAME
+ inner.innerHTML = column.innerHTML
+ inner.setAttribute('data-sb-created', true)
+ column.innerHTML = ''
+ column.append(inner)
+ }
+ })
+ }
+ balance (column, columnData, force, scrollDelta) {
+ /* Using the scroll position, container offset, and column
+ height, determine whether the column should be fixed or
+ absolute, and position it accordingly. */
- var topBreakpoint = this.containerTop - columnData.fixTop;
- // var bottomBreakpoint = this.containerTop + this.containerHeight -
- // this.winHeight + Math.max(
- // 0, this.winHeight - columnData.height - columnData.fixTop);
+ var state
+ var fixTop = columnData.fixTop
- var bottomBreakpoint = this.containerTop + this.containerHeight -
- columnData.height - columnData.fixTop;
+ if (scrollDelta === undefined) {
+ scrollDelta = 0
+ }
- if (this.scrollTop < topBreakpoint) {
- state = 'top';
- } else if (this.scrollTop > bottomBreakpoint) {
- state = 'bottom';
- } else {
- state = 'fixed';
- fixTop = columnData.fixTop - scrollDelta;
- fixTop = Math.max(columnData.minFixTop,
- Math.min(columnData.maxFixTop, fixTop));
- }
+ if (!columnData.enabled || !this.balance_enabled) {
+ state = 'disabled'
+ } else {
+ // determine state, one of
+ // - top
+ // - bottom
+ // - fixed
+
+ var topBreakpoint = this.containerTop - columnData.fixTop
+ // var bottomBreakpoint = this.containerTop + this.containerHeight -
+ // this.winHeight + Math.max(
+ // 0, this.winHeight - columnData.height - columnData.fixTop)
+
+ var bottomBreakpoint = this.containerTop + this.containerHeight -
+ columnData.height - columnData.fixTop
+
+ if (this.scrollTop < topBreakpoint) {
+ state = 'top'
+ } else if (this.scrollTop > bottomBreakpoint) {
+ state = 'bottom'
+ } else {
+ state = 'fixed'
+ fixTop = columnData.fixTop - scrollDelta
+ fixTop = Math.max(columnData.minFixTop,
+ Math.min(columnData.maxFixTop, fixTop))
}
+ }
- // update column positioning only if changed
- if (columnData.state !== state || columnData.fixTop !== fixTop || force) {
- var inner = col.find('.' + INNER_CLASSNAME);
- if (state === 'disabled') {
- inner.css({
- position: '',
- top: '',
- left: ''
- });
- } else if (state === 'fixed') {
- inner.css({
- position: 'fixed',
- top: fixTop,
- left: columnData.fixLeft - this.scrollLeft
- });
- } else {
- // assume one of "bottom" or "top"
- inner.css({
- position: 'absolute',
- top: (state === 'bottom' ? this.containerHeight -
- columnData.height : 0) + 'px',
- left: 0
- });
- }
- columnData.fixTop = fixTop;
- columnData.state = state;
- }
- },
- balance_all: function (force, scrollDelta) {
- /* Balance all columns */
- for (var i = 0; i < this.columns.length; i++) {
- if (this.columnData[i]) {
- this.balance(this.columns.eq(i), this.columnData[i], force,
- scrollDelta);
- }
+ // update column positioning only if changed
+ if (columnData.state !== state || columnData.fixTop !== fixTop || force) {
+ var inner = column.querySelector('.' + INNER_CLASSNAME)
+ if (state === 'disabled') {
+ inner.style.position = null
+ inner.style.top = null
+ inner.style.left = null
+ } else if (state === 'fixed') {
+ inner.style.position = 'fixed'
+ inner.style.top = `${fixTop}px`
+ inner.style.left = `${columnData.fixLeft - this.scrollLeft}px`
+ } else {
+ // assume one of "bottom" or "top"
+ inner.style.position = 'absolute'
+ inner.style.top = `${(state === 'bottom' ? this.containerHeight -
+ columnData.height : 0)}px`
+ inner.style.left = null
}
+ columnData.fixTop = fixTop
+ columnData.state = state
}
- };
-
- // export as jquery plugin
- $.fn.scrollbalance = function (options) {
- // the childSelector option is deprecated, but if set, it will be used
- // to find the columns, and this will be assumed to be a container,
- // for backwards compatibility.
- var columns;
- if (options && options.childSelector) {
- columns = this.find(options.childSelector);
- } else {
- columns = this;
+ }
+ balanceAll (force, scrollDelta) {
+ /* Balance all columns */
+ for (var i = 0; i < this.columns.length; i++) {
+ if (this.columnData[i]) {
+ this.balance(this.columns[i], this.columnData[i], force,
+ scrollDelta)
+ }
}
-
- var scrollbalance = new ScrollBalance(columns, options);
- scrollbalance.initialize();
- scrollbalance.bind();
-
- this.data('scrollbalance', scrollbalance);
- };
-
- return ScrollBalance;
-}));
+ }
+}