From 6fb2d14cc3b0b09407b25a3fa076ac060e7dfb09 Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Tue, 11 Dec 2018 15:53:54 +0000 Subject: [PATCH 1/6] Move shim handling to StickyElement Includes other code that splits the height StickyElement stores into two properties: 1. `verticalSpace` 2. `height` `verticalSpace` is the vertical space the element occupies in the document flow when not stuck. `height` is the visual space of the element, including padding and border. --- .../stick-to-window-when-scrolling.js | 95 ++++++++++++------- 1 file changed, 63 insertions(+), 32 deletions(-) diff --git a/app/assets/javascripts/stick-to-window-when-scrolling.js b/app/assets/javascripts/stick-to-window-when-scrolling.js index 9838570d8..31088225a 100644 --- a/app/assets/javascripts/stick-to-window-when-scrolling.js +++ b/app/assets/javascripts/stick-to-window-when-scrolling.js @@ -10,6 +10,7 @@ this._initialFixedClass = 'content-fixed-onload'; this._fixedClass = 'content-fixed'; this._appliedClass = null; + this._$shim = null; this._stopped = false; this.scrolledFrom = this._sticky.getScrolledFrom($el); }; @@ -30,6 +31,14 @@ this._appliedClass = null; this._hasBeenCalled = true; }; + StickyElement.prototype.addShim = function (position) { + this._$shim = $('
 
'); + this.$fixedEl[position](this._$shim); + }; + StickyElement.prototype.removeShim = function () { + this._$shim.remove(); + this._$shim = null; + }; StickyElement.prototype.stop = function () { this._stopped = true; }; @@ -87,6 +96,19 @@ if (self._initialPositionsSet === false) { self._initialPositionsSet = true; } }; + Sticky.prototype.setElementDimensions = function (el, callback) { + var self = this; + var onHeightSet = function () { + el.scrolledTo = self.getScrollingTo(el); + if (callback !== undefined) { + callback(); + } + }; + + this.setFixedTop(el); + this.setElWidth(el); + this.setElHeight(el, onHeightSet); + }; Sticky.prototype.setFixedTop = function (el) { var $siblingEl = $('
'); $siblingEl.insertBefore(el.$fixedEl); @@ -95,7 +117,10 @@ el.fixedTop = fixedTop; }; - Sticky.prototype.setElHeight = function (el) { + Sticky.prototype.setElWidth = function (el) { + el.horizontalSpace = el.$fixedEl.outerWidth(true); + }; + Sticky.prototype.setElHeight = function (el, callback) { var self = this; var fixedOffset = parseInt(el.$fixedEl.css('top'), 10); var $el = el.$fixedEl; @@ -106,37 +131,40 @@ if ((!self._elsLoaded) && ($img.length > 0)) { var image = new global.Image(); image.onload = function () { - el.height = $el.outerHeight() + fixedOffset; - el.scrolledTo = self.getScrollingTo(el); - self.checkElementsLoaded(); + el.verticalSpace = $el.outerHeight(true) + fixedOffset; + el.height = $el.outerHeight(); + callback(); }; image.src = $img.attr('src'); } else { - el.height = $el.outerHeight() + fixedOffset; - el.scrolledTo = self.getScrollingTo(el); - self.checkElementsLoaded(); + el.verticalSpace = $el.outerHeight() + fixedOffset; + el.height = $el.outerHeight(); + callback(); } }; - Sticky.prototype.checkElementsLoaded = function () { - this._elsLoaded = $.grep(this._els, function (el) { return ('height' in el); }).length === this._els.length; + Sticky.prototype.allElementsLoaded = function (totalEls) { + return this._els.length === totalEls; }; Sticky.prototype.init = function () { var self = this; var $els = $(self.CSS_SELECTOR); + var numOfEls = $els.length; - if ($els.length > 0) { + if (numOfEls > 0) { $els.each(function (i, el) { var $el = $(el); var elObj = new StickyElement($el, self); - self.setFixedTop(elObj); - self.setElHeight(elObj); - self._els.push(elObj); + self.setElementDimensions(elObj, function () { + self._els.push(elObj); + // set positions based on initial scroll positionu + if (self._els.length === numOfEls) { + self._elsLoaded = true; + self.setElementPositions(); + } + }); }); - // set element positions based on page scroll position on load - self.setElementPositions(); - if (self._scrollTimeout === false) { $(global).scroll(function (e) { self.onScroll(); }); self._scrollTimeout = global.setInterval(function (e) { self.checkScroll(); }, 50); @@ -192,23 +220,12 @@ }); } }; - Sticky.prototype.stick = function (el) { - if (!el.isStuck()) { - var $el = el.$fixedEl; - var height = Math.max($el.height(), 1); - var width = $el.width(); - - this.addShimForEl($el, width, height); - $el.css('width', width + 'px').addClass(el.stickyClass()); - el.stick(); - } - }; Sticky.prototype.release = function (el) { if (el.isStuck()) { var $el = el.$fixedEl; $el.removeClass(el.appliedClass()).css('width', ''); - $el.siblings('.shim').remove(); + el.removeShim(); el.release(); } }; @@ -234,8 +251,15 @@ return windowTop > el.scrolledTo; }; - stickAtTop.addShimForEl = function ($el, width, height) { - $el.before('
 
'); + stickAtTop.stick = function (el) { + if (!el.isStuck()) { + var $el = el.$fixedEl; + + el.addShim('before'); + // element will be absolutely positioned so cannot rely on parent element for width + $el.css('width', $el.width() + 'px').addClass(el.stickyClass()); + el.stick(); + } }; stickAtTop.stop = function (el) { if (!el.stopped()) { @@ -271,8 +295,15 @@ return windowBottom < el.scrolledTo; }; - stickAtBottom.addShimForEl = function ($el, width, height) { - $el.after('
 
'); + stickAtBottom.stick = function (el) { + if (!el.isStuck()) { + var $el = el.$fixedEl; + + el.addShim('after'); + // element will be absolutely positioned so cannot rely on parent element for width + el.$fixedEl.css('width', $el.width() + 'px').addClass(el.stickyClass()); + el.stick(); + } }; stickAtBottom.stop = function (el) { if (!el.stopped()) { From 9823ff831f48b06504b9fb946650bf54fab483fd Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Thu, 13 Dec 2018 14:21:51 +0000 Subject: [PATCH 2/6] Add publich recalculate method Allows other JS to tell sticky elements to recalculate their dimensions and then position (and then check to see if their state needs changing). We need this because we change the content of our element so its dimensions change. The recalculation code also updates the shim for elements that are 'stuck' so the horizontal space the element would occupy in the flow of the page is correct. --- .../stick-to-window-when-scrolling.js | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/app/assets/javascripts/stick-to-window-when-scrolling.js b/app/assets/javascripts/stick-to-window-when-scrolling.js index 31088225a..4761c84fd 100644 --- a/app/assets/javascripts/stick-to-window-when-scrolling.js +++ b/app/assets/javascripts/stick-to-window-when-scrolling.js @@ -39,6 +39,14 @@ this._$shim.remove(); this._$shim = null; }; + StickyElement.prototype.updateShim = function () { + if (this._$shim) { + this._$shim.css({ + 'height': this.verticalSpace, + 'width': this.horizontalSpace + }); + } + }; StickyElement.prototype.stop = function () { this._stopped = true; }; @@ -100,6 +108,7 @@ var self = this; var onHeightSet = function () { el.scrolledTo = self.getScrollingTo(el); + el.updateShim(el); if (callback !== undefined) { callback(); } @@ -109,6 +118,14 @@ this.setElWidth(el); this.setElHeight(el, onHeightSet); }; + Sticky.prototype.recalculate = function () { + var self = this; + + $.each(self._els, function (i, el) { + self.setElementDimensions(el); + }); + self.setElementPositions(); + }; Sticky.prototype.setFixedTop = function (el) { var $siblingEl = $('
'); $siblingEl.insertBefore(el.$fixedEl); @@ -122,22 +139,19 @@ }; Sticky.prototype.setElHeight = function (el, callback) { var self = this; - var fixedOffset = parseInt(el.$fixedEl.css('top'), 10); var $el = el.$fixedEl; var $img = $el.find('img'); - fixedOffset = isNaN(fixedOffset) ? 0 : fixedOffset; - if ((!self._elsLoaded) && ($img.length > 0)) { var image = new global.Image(); image.onload = function () { - el.verticalSpace = $el.outerHeight(true) + fixedOffset; + el.verticalSpace = $el.outerHeight(true); el.height = $el.outerHeight(); callback(); }; image.src = $img.attr('src'); } else { - el.verticalSpace = $el.outerHeight() + fixedOffset; + el.verticalSpace = $el.outerHeight(true); el.height = $el.outerHeight(); callback(); } From 030701ab1b57de66e4eba5ac1b08bc34b0de4708 Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Thu, 13 Dec 2018 14:25:22 +0000 Subject: [PATCH 3/6] Make folder controls update sticky JS on change Also removes a call the `render` method which duplicates one already made by the `selectActionButtons` method on load. --- app/assets/javascripts/stick-to-window-when-scrolling.js | 9 +++++++-- app/assets/javascripts/templateFolderForm.js | 7 +++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/stick-to-window-when-scrolling.js b/app/assets/javascripts/stick-to-window-when-scrolling.js index 4761c84fd..db3314e83 100644 --- a/app/assets/javascripts/stick-to-window-when-scrolling.js +++ b/app/assets/javascripts/stick-to-window-when-scrolling.js @@ -12,7 +12,6 @@ this._appliedClass = null; this._$shim = null; this._stopped = false; - this.scrolledFrom = this._sticky.getScrolledFrom($el); }; StickyElement.prototype.stickyClass = function () { return (this._sticky._initialPositionsSet) ? this._fixedClass : this._initialFixedClass; @@ -106,9 +105,15 @@ }; Sticky.prototype.setElementDimensions = function (el, callback) { var self = this; + var $el = el.$fixedEl; var onHeightSet = function () { el.scrolledTo = self.getScrollingTo(el); - el.updateShim(el); + // if element is shim'ed, pass changes in dimension on to the shim + if (el._$shim) { + el.updateShim(); + $el = el._$shim; + } + el.scrolledFrom = self.getScrolledFrom($el); if (callback !== undefined) { callback(); } diff --git a/app/assets/javascripts/templateFolderForm.js b/app/assets/javascripts/templateFolderForm.js index 744b0c4f6..d9f953acc 100644 --- a/app/assets/javascripts/templateFolderForm.js +++ b/app/assets/javascripts/templateFolderForm.js @@ -36,8 +36,6 @@ this.$form.on('click', 'button.button-secondary', (event) => this.actionButtonClicked(event)); this.$form.on('change', 'input[type=checkbox]', () => this.templateFolderCheckboxChanged()); - - this.render(); }; this.addCancelButton = function(state) { @@ -96,6 +94,11 @@ this.states.forEach( state => (state.key === this.currentState ? this.$stickyBottom.append(state.$el) : state.$el.detach()) ); + + // make sticky JS recalculate its cache of the element's position + if ('stickAtBottomWhenScrolling' in GOVUK) { + GOVUK.stickAtBottomWhenScrolling.recalculate(); + } }; this.nothingSelectedButtons = ` From 108cec7938a5725b0b194ca6e0cdfe9e4b829232 Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Thu, 13 Dec 2018 18:38:42 +0000 Subject: [PATCH 4/6] Remove fixedTop property from sticky elements It's no longer used by this code. --- app/assets/javascripts/stick-to-window-when-scrolling.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/assets/javascripts/stick-to-window-when-scrolling.js b/app/assets/javascripts/stick-to-window-when-scrolling.js index db3314e83..7d1e0907e 100644 --- a/app/assets/javascripts/stick-to-window-when-scrolling.js +++ b/app/assets/javascripts/stick-to-window-when-scrolling.js @@ -119,7 +119,6 @@ } }; - this.setFixedTop(el); this.setElWidth(el); this.setElHeight(el, onHeightSet); }; @@ -131,14 +130,6 @@ }); self.setElementPositions(); }; - Sticky.prototype.setFixedTop = function (el) { - var $siblingEl = $('
'); - $siblingEl.insertBefore(el.$fixedEl); - var fixedTop = $siblingEl.offset().top - $siblingEl.position().top; - $siblingEl.remove(); - - el.fixedTop = fixedTop; - }; Sticky.prototype.setElWidth = function (el) { el.horizontalSpace = el.$fixedEl.outerWidth(true); }; From b55acc498151ec38d34bc12653fc95d15adac644 Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Thu, 13 Dec 2018 18:59:34 +0000 Subject: [PATCH 5/6] Add comments --- .../stick-to-window-when-scrolling.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app/assets/javascripts/stick-to-window-when-scrolling.js b/app/assets/javascripts/stick-to-window-when-scrolling.js index 7d1e0907e..7df6744c6 100644 --- a/app/assets/javascripts/stick-to-window-when-scrolling.js +++ b/app/assets/javascripts/stick-to-window-when-scrolling.js @@ -4,6 +4,7 @@ var $ = global.jQuery; var GOVUK = global.GOVUK || {}; + // Constructor for objects holding data for each element to have sticky behaviour var StickyElement = function ($el, sticky) { this._sticky = sticky; this.$fixedEl = $el; @@ -30,6 +31,8 @@ this._appliedClass = null; this._hasBeenCalled = true; }; + // When a sticky element is moved into the 'stuck' state, a shim is inserted into the + // page to preserve the space the element occupies in the flow. StickyElement.prototype.addShim = function (position) { this._$shim = $('
 
'); this.$fixedEl[position](this._$shim); @@ -38,6 +41,7 @@ this._$shim.remove(); this._$shim = null; }; + // Changes to the dimensions of a sticky element with a shim need to be passed on to the shim StickyElement.prototype.updateShim = function () { if (this._$shim) { this._$shim.css({ @@ -56,7 +60,8 @@ return this._stopped; }; - // Stick elements to top of screen when you scroll past, documentation is in the README.md + // Constructor for objects collecting together all generic behaviour for controlling the state of + // sticky elements var Sticky = function (selector) { this._hasScrolled = false; this._scrollTimeout = false; @@ -79,6 +84,7 @@ scrollTop: $(global).scrollTop() }; }; + // Change state of sticky elements based on their position relative to the window Sticky.prototype.setElementPositions = function () { var self = this; @@ -103,6 +109,7 @@ if (self._initialPositionsSet === false) { self._initialPositionsSet = true; } }; + // Store all the dimensions for a sticky element to limit DOM queries Sticky.prototype.setElementDimensions = function (el, callback) { var self = this; var $el = el.$fixedEl; @@ -122,6 +129,7 @@ this.setElWidth(el); this.setElHeight(el, onHeightSet); }; + // Recalculate stored dimensions for all sticky elements Sticky.prototype.recalculate = function () { var self = this; @@ -175,11 +183,14 @@ }); }); + // flag when scrolling takes place and check (and re-position) sticky elements relative to + // window position if (self._scrollTimeout === false) { $(global).scroll(function (e) { self.onScroll(); }); self._scrollTimeout = global.setInterval(function (e) { self.checkScroll(); }, 50); } + // Recalculate all dimensions when the window resizes if (self._resizeTimeout === false) { $(global).resize(function (e) { self.onResize(); }); self._resizeTimeout = global.setInterval(function (e) { self.checkResize(); }, 50); @@ -240,10 +251,13 @@ } }; + // Extension of sticky object to add behaviours specific to sticking to top of window var stickAtTop = new Sticky('.js-stick-at-top-when-scrolling'); + // Store top of sticky elements while unstuck stickAtTop.getScrolledFrom = function ($el) { return $el.offset().top; }; + // Store furthest point top of sticky element is allowed stickAtTop.getScrollingTo = function (el) { var footer = $('.js-footer:eq(0)'); if (footer.length === 0) { @@ -284,10 +298,13 @@ } }; + // Extension of sticky object to add behaviours specific to sticking to bottom of window var stickAtBottom = new Sticky('.js-stick-at-bottom-when-scrolling'); + // Store bottom of sticky elements while unstuck stickAtBottom.getScrolledFrom = function ($el) { return $el.offset().top + $el.outerHeight(); }; + // Store furthest point bottom of sticky element is allowed stickAtBottom.getScrollingTo = function (el) { var header = $('.js-header:eq(0)'); if (header.length === 0) { From 573d2d1d655cc912dfd908f0ca9053f185bfdcec Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Fri, 14 Dec 2018 16:07:06 +0000 Subject: [PATCH 6/6] Fix code for sticky JS when window resizes Includes: - change .stopped method to .isStopped() for consistency - replace code in checkResize that adjusts dimensions for setElementDimensions - add code that deals with the window size being too small to run whenever positions are calculated - add reset method for when screen is too small for sticking behaviour - move guard out of methods for stopping and sticking (it hid info that was useful at the point they were called) --- .../stick-to-window-when-scrolling.js | 142 ++++++++++-------- 1 file changed, 76 insertions(+), 66 deletions(-) diff --git a/app/assets/javascripts/stick-to-window-when-scrolling.js b/app/assets/javascripts/stick-to-window-when-scrolling.js index 7df6744c6..c7296d12d 100644 --- a/app/assets/javascripts/stick-to-window-when-scrolling.js +++ b/app/assets/javascripts/stick-to-window-when-scrolling.js @@ -51,13 +51,13 @@ } }; StickyElement.prototype.stop = function () { - this._stopped = true; + this._isStopped = true; }; StickyElement.prototype.unstop = function () { - this._stopped = false; + this._isStopped = false; }; - StickyElement.prototype.stopped = function () { - return this._stopped; + StickyElement.prototype.isStopped = function () { + return this._isStopped; }; // Constructor for objects collecting together all generic behaviour for controlling the state of @@ -89,21 +89,30 @@ var self = this; $.each(self._els, function (i, el) { - var $el = el.$fixedEl; + var $el = el.$fixedEl, + windowDimensions = self.getWindowDimensions(); - var windowDimensions = self.getWindowDimensions(); + if (self.viewportIsWideEnough(windowDimensions.width)) { - if (self.scrolledFromInsideWindow(el.scrolledFrom)) { - self.release(el); - } else { - if (self.scrolledToOutsideWindow(el, windowDimensions.height)) { - self.stop(el); - } else if (self.viewportIsWideEnough(windowDimensions.width)) { - if (el.stopped) { - self.unstop(el); + if (self.windowNotPastScrolledFrom(el.scrolledFrom)) { + self.release(el); + } else { + if (self.windowNotPastScrolledTo(el, windowDimensions.height)) { + self.stick(el); + if (el.isStopped()) { + self.unstop(el); + } + } else { // window past scrolledTo position + if (!el.isStopped()) { + self.stop(el); + } } - self.stick(el); } + + } else { + + self.reset(el); + } }); @@ -129,6 +138,15 @@ this.setElWidth(el); this.setElHeight(el, onHeightSet); }; + // Reset element to original state in the page + Sticky.prototype.reset = function (el) { + if (el.isStuck()) { + this.release(el); + } + if (el.isStopped()) { + this.unstop(el); + } + }; // Recalculate stored dimensions for all sticky elements Sticky.prototype.recalculate = function () { var self = this; @@ -139,7 +157,14 @@ self.setElementPositions(); }; Sticky.prototype.setElWidth = function (el) { - el.horizontalSpace = el.$fixedEl.outerWidth(true); + var $el = el.$fixedEl; + var width = $el.parent().width(); + + el.horizontalSpace = width; + // if stuck, element won't inherit width from parent so set explicitly + if (el._$shim) { + $el.width(width); + } }; Sticky.prototype.setElHeight = function (el, callback) { var self = this; @@ -197,15 +222,15 @@ } } }; + Sticky.prototype.viewportIsWideEnough = function (windowWidth) { + return windowWidth > 768; + }; Sticky.prototype.onScroll = function () { this._hasScrolled = true; }; Sticky.prototype.onResize = function () { this._hasResized = true; }; - Sticky.prototype.viewportIsWideEnough = function (windowWidth) { - return windowWidth > 768; - }; Sticky.prototype.checkScroll = function () { var self = this; @@ -215,30 +240,23 @@ } }; Sticky.prototype.checkResize = function () { - var self = this; + var self = this, + windowWidth = self.getWindowDimensions().width; if (self._hasResized === true) { self._hasResized = false; - var windowDimensions = self.getWindowDimensions(); - $.each(self._els, function (i, el) { - var $el = el.$fixedEl; - - var elResize = $el.hasClass('js-self-resize'); - if (elResize) { - var $shim = $('.shim'); - var $elParent = $el.parent('div'); - var elParentWidth = $elParent.width(); - $shim.css('width', elParentWidth); - $el.css('width', elParentWidth); - self.setElHeight(el); - } - - if (!self.viewportIsWideEnough(windowDimensions.width)) { - self.release($el); + if (!self.viewportIsWideEnough(windowWidth)) { + self.reset(el); + } else { + self.setElementDimensions(el); } }); + + if (self.viewportIsWideEnough(windowWidth)) { + self.setElementPositions(); + } } }; Sticky.prototype.release = function (el) { @@ -265,15 +283,15 @@ } return (footer.offset().top - 10) - el.height; }; - stickAtTop.scrolledFromInsideWindow = function (scrolledFrom) { + stickAtTop.windowNotPastScrolledFrom = function (scrolledFrom) { var windowTop = this.getWindowPositions().scrollTop; return scrolledFrom > windowTop; }; - stickAtTop.scrolledToOutsideWindow = function (el, windowHeight) { + stickAtTop.windowNotPastScrolledTo = function (el, windowHeight) { var windowTop = this.getWindowPositions().scrollTop; - return windowTop > el.scrolledTo; + return windowTop < el.scrolledTo; }; stickAtTop.stick = function (el) { if (!el.isStuck()) { @@ -286,16 +304,12 @@ } }; stickAtTop.stop = function (el) { - if (!el.stopped()) { - el.$fixedEl.css({ 'position': 'absolute', 'top': el.scrolledTo }); - el.stop(); - } + el.$fixedEl.css({ 'position': 'absolute', 'top': el.scrolledTo }); + el.stop(); }; stickAtTop.unstop = function (el) { - if (el.stopped()) { - el.$fixedEl.css({ 'position': '', 'top': '' }); - el.unstop(); - } + el.$fixedEl.css({ 'position': '', 'top': '' }); + el.unstop(); }; // Extension of sticky object to add behaviours specific to sticking to bottom of window @@ -312,15 +326,15 @@ } return (header.offset().top + header.outerHeight() + 10) + el.height; }; - stickAtBottom.scrolledFromInsideWindow = function (scrolledFrom) { + stickAtBottom.windowNotPastScrolledFrom = function (scrolledFrom) { var windowBottom = this.getWindowPositions().scrollTop + this.getWindowDimensions().height; return scrolledFrom < windowBottom; }; - stickAtBottom.scrolledToOutsideWindow = function (el, windowHeight) { + stickAtBottom.windowNotPastScrolledTo = function (el, windowHeight) { var windowBottom = this.getWindowPositions().scrollTop + this.getWindowDimensions().height; - return windowBottom < el.scrolledTo; + return windowBottom > el.scrolledTo; }; stickAtBottom.stick = function (el) { if (!el.isStuck()) { @@ -333,24 +347,20 @@ } }; stickAtBottom.stop = function (el) { - if (!el.stopped()) { - el.$fixedEl.css({ - 'position': 'absolute', - 'top': (el.scrolledTo - el.height), - 'bottom': 'auto' - }); - el.stop(); - } + el.$fixedEl.css({ + 'position': 'absolute', + 'top': (el.scrolledTo - el.height), + 'bottom': 'auto' + }); + el.stop(); }; stickAtBottom.unstop = function (el) { - if (el.stopped()) { - el.$fixedEl.css({ - 'position': '', - 'top': '', - 'bottom': '' - }); - el.unstop(); - } + el.$fixedEl.css({ + 'position': '', + 'top': '', + 'bottom': '' + }); + el.unstop(); }; GOVUK.stickAtTopWhenScrolling = stickAtTop;