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; -})); + } +}