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;