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;