From deaa253e658adae04c0e6caf1443a862fcc3f2a5 Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Mon, 5 Nov 2018 17:47:25 +0000 Subject: [PATCH] Add stop-at-bottom functionality Detach all methods from sticky reference so they can be attached to different objects. Split sticky into stickAtTop and stickAtBottom and make new versions of all methods and properties specific to stickAtBottom. Add CSS for stickAtBottom and call on load --- app/assets/javascripts/main.js | 1 + .../stick-at-top-when-scrolling.js | 349 +++++++++++------- .../stick-at-top-when-scrolling.scss | 47 ++- 3 files changed, 263 insertions(+), 134 deletions(-) diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index c04229cac..33ef38a0c 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -1,6 +1,7 @@ $(() => $("time.timeago").timeago()); $(() => GOVUK.stickAtTopWhenScrolling.init()); +$(() => GOVUK.stickAtBottomWhenScrolling.init()); var showHideContent = new GOVUK.ShowHideContent(); showHideContent.init(); diff --git a/app/assets/javascripts/stick-at-top-when-scrolling.js b/app/assets/javascripts/stick-at-top-when-scrolling.js index 964ba2b9b..8541d8a09 100644 --- a/app/assets/javascripts/stick-at-top-when-scrolling.js +++ b/app/assets/javascripts/stick-at-top-when-scrolling.js @@ -5,142 +5,243 @@ var GOVUK = global.GOVUK || {} // Stick elements to top of screen when you scroll past, documentation is in the README.md - var sticky = { - _hasScrolled: false, - _scrollTimeout: false, - _hasResized: false, - _resizeTimeout: false, - _els: [], + var Sticky = function (selector) { + this._hasScrolled: false + this._scrollTimeout: false + this._hasResized: false + this._resizeTimeout: false + this._els: [] - getWindowDimensions: function () { - return { - height: $(global).height(), - width: $(global).width() - } - }, - getWindowPositions: function () { - return { - scrollTop: $(global).scrollTop() - } - }, - getElementOffset: function ($el) { - return $el.offset() - }, - setElHeight: function (el) { - var fixedOffset = parseInt(el.$fixedEl.css('top'), 10) - var $el = el.$fixedEl - var $img = $el.find('img') + this.CSS_SELECTOR: selector + } + Sticky.prototype.getWindowDimensions = function () { + return { + height: $(global).height(), + width: $(global).width() + } + } + Sticky.prototype.getWindowPositions = function () { + return { + scrollTop: $(global).scrollTop() + } + } + Sticky.prototype.setFixedTop = function (el) { + var $siblingEl = $('
') + $siblingEl.insertBefore(el.$fixedEl) + var fixedTop = $siblingEl.offset().top - $siblingEl.position().top + $siblingEl.remove() - fixedOffset = isNaN(fixedOffset) ? 0 : fixedOffset + el.fixedTop = fixedTop + } + Sticky.prototype.setElHeight = function (el) { + var self = this + var fixedOffset = parseInt(el.$fixedEl.css('top'), 10) + var $el = el.$fixedEl + var $img = $el.find('img') - if ($img.length > 0) { - var image = new global.Image() - image.onload = function () { - el.height = $el.outerHeight() + fixedOffset - } - image.src = $img.attr('src') - } else { + fixedOffset = isNaN(fixedOffset) ? 0 : fixedOffset + + if ($img.length > 0) { + var image = new global.Image() + image.onload = function () { el.height = $el.outerHeight() + fixedOffset + el.scrolledTo = self.getScrollingTo(el) } - }, - init: function () { - var $els = $('.js-stick-at-top-when-scrolling') + image.src = $img.attr('src') + } else { + el.height = $el.outerHeight() + fixedOffset + el.scrolledTo = self.getScrollingTo(el) + } + } + Sticky.prototype.init = function () { + var self = this + var $els = $(self.CSS_SELECTOR) - if ($els.length > 0) { - $els.each(function (i, el) { - var $el = $(el) - var el = { - $fixedEl: $el - } - - sticky.setElHeight(el) - sticky._els.push(el) - $el.data('scrolled-from', sticky.getElementOffset($el).top) - }) - - if (sticky._scrollTimeout === false) { - $(global).scroll(sticky.onScroll) - sticky._scrollTimeout = global.setInterval(sticky.checkScroll, 50) + if ($els.length > 0) { + $els.each(function (i, el) { + var $el = $(el) + var el = { + $fixedEl: $el, + scrolledFrom: self.getScrolledFrom($el), + stopped: false } - if (sticky._resizeTimeout === false) { - $(global).resize(sticky.onResize) - sticky._resizeTimeout = global.setInterval(sticky.checkResize, 50) - } + self.setFixedTop(el) + self.setElHeight(el) + self._els.push(el) + }) + + if (self._scrollTimeout === false) { + $(global).scroll(function (e) { self.onScroll() }) + self._scrollTimeout = global.setInterval(function (e) { self.checkScroll() }, 50) } - }, - onScroll: function () { - sticky._hasScrolled = true - }, - onResize: function () { - sticky._hasResized = true - }, - scrolledFromInsideWindow: function (scrolledFrom) { - var windowVerticalPosition = sticky.getWindowPositions().scrollTop - return windowVerticalPosition < scrolledFrom - }, - checkScroll: function () { - if (sticky._hasScrolled === true) { - sticky._hasScrolled = false - - var windowDimensions = sticky.getWindowDimensions() - - $.each(sticky._els, function (i, el) { - var $el = el.$fixedEl - var scrolledFrom = $el.data('scrolled-from') - - if (scrolledFrom && sticky.scrolledFromInsideWindow(scrolledFrom)) { - sticky.release($el) - } else if (windowDimensions.width > 768 && !sticky.scrolledFromInsideWindow(scrolledFrom)) { - sticky.stick($el) - } - }) - } - }, - checkResize: function () { - if (sticky._hasResized === true) { - sticky._hasResized = false - - var windowDimensions = sticky.getWindowDimensions() - - $.each(sticky._els, function (i, el) { - var $el = el.$fixedEl - - var elResize = $el.hasClass('js-sticky-resize') - if (elResize) { - var $shim = $('.shim') - var $elParent = $el.parent('div') - var elParentWidth = $elParent.width() - $shim.css('width', elParentWidth) - $el.css('width', elParentWidth) - } - - if (windowDimensions.width <= 768) { - sticky.release($el) - } - }) - } - }, - addShimForEl: function ($el, width, height) { - $el.before('
 
') - }, - stick: function ($el) { - if (!$el.hasClass('content-fixed')) { - var height = Math.max($el.height(), 1) - var width = $el.width() - - sticky.addShimForEl($el, width, height) - $el.css('width', width + 'px').addClass('content-fixed') - } - }, - release: function ($el) { - if ($el.hasClass('content-fixed')) { - $el.removeClass('content-fixed').css('width', '') - $el.siblings('.shim').remove() + if (self._resizeTimeout === false) { + $(global).resize(function (e) { self.onResize() }) + self._resizeTimeout = global.setInterval(function (e) { self.checkResize() }, 50) } } } - GOVUK.stickAtTopWhenScrolling = sticky + 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 + + if (self._hasScrolled === true) { + self._hasScrolled = false + + $.each(self._els, function (i, el) { + var $el = el.$fixedEl + + var windowDimensions = self.getWindowDimensions() + + 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) + } + self.stick($el) + } + } + }) + } + } + Sticky.prototype.checkResize = function () { + var self = this + + 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) + } + + if (windowDimensions.width <= 768) { + self.release($el) + } + }) + } + } + Sticky.prototype.stick = function ($el) { + if (!$el.hasClass('content-fixed')) { + var height = Math.max($el.height(), 1) + var width = $el.width() + + this.addShimForEl($el, width, height) + $el.css('width', width + 'px').addClass('content-fixed') + } + } + Sticky.prototype.release = function ($el) { + if ($el.hasClass('content-fixed')) { + $el.removeClass('content-fixed').css('width', '') + $el.siblings('.shim').remove() + } + } + + var stickAtTop = new Sticky('.js-stick-at-top-when-scrolling') + stickAtTop.getScrolledFrom = function ($el) { + return $el.offset().top + } + stickAtTop.getScrollingTo = function (el) { + var footer = $('.js-footer:eq(0)') + if (footer.length === 0) { + return 0 + } + return (footer.offset().top - 10) - el.height + } + stickAtTop.scrolledFromInsideWindow = function (scrolledFrom) { + var windowTop = this.getWindowPositions().scrollTop + + return scrolledFrom > windowTop + } + stickAtTop.scrolledToOutsideWindow = function (el, windowHeight) { + var windowTop = this.getWindowPositions().scrollTop + + return windowTop > el.scrolledTo + } + stickAtTop.addShimForEl = function ($el, width, height) { + $el.before('
 
') + } + stickAtTop.stop = function (el) { + if (!el.stopped) { + el.$fixedEl.css({ 'position': 'absolute', 'top': el.scrolledTo }) + el.stopped = true + } + } + stickAtTop.unstop = function (el) { + if (el.stopped) { + el.$fixedEl.css({ 'position': '', 'top': '' }) + el.stopped = false + } + } + + var stickAtBottom = new Sticky('.js-stick-at-bottom-when-scrolling') + stickAtBottom.getScrolledFrom = function ($el) { + return $el.offset().top + $el.outerHeight() + } + stickAtBottom.getScrollingTo = function (el) { + var header = $('.js-header:eq(0)') + if (header.length === 0) { + return 0 + } + return (header.offset().top + header.outerHeight() + 10) + el.height + } + stickAtBottom.scrolledFromInsideWindow = function (scrolledFrom) { + var windowBottom = this.getWindowPositions().scrollTop + this.getWindowDimensions().height + + return scrolledFrom < windowBottom + } + stickAtBottom.scrolledToOutsideWindow = function (el, windowHeight) { + var windowBottom = this.getWindowPositions().scrollTop + this.getWindowDimensions().height + + return windowBottom < el.scrolledTo + } + stickAtBottom.addShimForEl = function ($el, width, height) { + $el.after('
 
') + } + stickAtBottom.stop = function (el) { + if (!el.stopped) { + el.$fixedEl.css({ + 'position': 'absolute', + 'top': (el.scrolledTo - el.height), + 'bottom': 'auto' + }) + el.stopped = true + } + } + stickAtBottom.unstop = function (el) { + if (el.stopped) { + el.$fixedEl.css({ + 'position': '', + 'top': '', + 'bottom': '' + }) + el.stopped = false + } + } + + GOVUK.stickAtTopWhenScrolling = stickAtTop + GOVUK.stickAtBottomWhenScrolling = stickAtBottom global.GOVUK = GOVUK })(window) diff --git a/app/assets/stylesheets/components/stick-at-top-when-scrolling.scss b/app/assets/stylesheets/components/stick-at-top-when-scrolling.scss index 77d076ff1..516e44a13 100644 --- a/app/assets/stylesheets/components/stick-at-top-when-scrolling.scss +++ b/app/assets/stylesheets/components/stick-at-top-when-scrolling.scss @@ -1,16 +1,13 @@ // CSS adapted from // https://github.com/alphagov/govuk_frontend_toolkit/blob/d9489a987086471fe30b4b925a81c12cd198c91d/docs/javascript.md#stick-at-top-when-scrolling -.js-stick-at-top-when-scrolling { +.js-stick-at-top-when-scrolling, +.js-stick-at-bottom-when-scrolling { overflow: hidden; margin-left: -$gutter-half; - margin-top: -10px; - margin-bottom: 5px; padding: 10px 0 0 $gutter-half; position: relative; - top: 5px; - transition: top 0.1s ease-out, box-shadow 1s ease-in-out; .form-group { margin-bottom: 20px; @@ -32,17 +29,27 @@ } +.js-stick-at-top-when-scrolling { + + margin-top: -10px; + margin-bottom: 5px; + top: 5px; + transition: top 0.1s ease-out, box-shadow 1s ease-in-out; + +} + +.js-stick-at-top-when-scrolling { + + transition: bottom 0.1s ease-out, box-shadow 1s ease-in-out; + +} + .content-fixed { position: fixed; - top: 0; background: $white; z-index: 100; - margin-top: 0; padding-right: $gutter-half; - border-bottom: 1px solid $border-colour; - box-shadow: 0 2px 0 0 rgba($border-colour, 0.2); - transition: background 0.6s ease-in-out, margin-top 0.4s ease-out; .back-to-top-link { opacity: 1; @@ -51,6 +58,26 @@ } +.js-stick-at-top-when-scrolling.content-fixed { + + top: 0; + margin-top: 0; + border-bottom: 1px solid $border-colour; + box-shadow: 0 2px 0 0 rgba($border-colour, 0.2); + transition: background 0.6s ease-in-out, margin-top 0.4s ease-out; + +} + +.js-stick-at-bottom-when-scrolling.content-fixed { + + top: auto; // cancel `top: 0;` inherited from govuk-template + bottom: 0; + border-top: 1px solid $border-colour; + box-shadow: 0 -2px 0 0 rgba($border-colour, 0.2); + transition: background 0.6s ease-in-out; + +} + .shim { display: block; margin-bottom: 5px;