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
This commit is contained in:
Tom Byers
2018-11-05 17:47:25 +00:00
parent ef76d4eeb3
commit deaa253e65
3 changed files with 263 additions and 134 deletions

View File

@@ -1,6 +1,7 @@
$(() => $("time.timeago").timeago()); $(() => $("time.timeago").timeago());
$(() => GOVUK.stickAtTopWhenScrolling.init()); $(() => GOVUK.stickAtTopWhenScrolling.init());
$(() => GOVUK.stickAtBottomWhenScrolling.init());
var showHideContent = new GOVUK.ShowHideContent(); var showHideContent = new GOVUK.ShowHideContent();
showHideContent.init(); showHideContent.init();

View File

@@ -5,142 +5,243 @@
var GOVUK = global.GOVUK || {} var GOVUK = global.GOVUK || {}
// Stick elements to top of screen when you scroll past, documentation is in the README.md // Stick elements to top of screen when you scroll past, documentation is in the README.md
var sticky = { var Sticky = function (selector) {
_hasScrolled: false, this._hasScrolled: false
_scrollTimeout: false, this._scrollTimeout: false
_hasResized: false, this._hasResized: false
_resizeTimeout: false, this._resizeTimeout: false
_els: [], this._els: []
getWindowDimensions: function () { this.CSS_SELECTOR: selector
return { }
height: $(global).height(), Sticky.prototype.getWindowDimensions = function () {
width: $(global).width() return {
} height: $(global).height(),
}, width: $(global).width()
getWindowPositions: function () { }
return { }
scrollTop: $(global).scrollTop() Sticky.prototype.getWindowPositions = function () {
} return {
}, scrollTop: $(global).scrollTop()
getElementOffset: function ($el) { }
return $el.offset() }
}, Sticky.prototype.setFixedTop = function (el) {
setElHeight: function (el) { var $siblingEl = $('<div></div>')
var fixedOffset = parseInt(el.$fixedEl.css('top'), 10) $siblingEl.insertBefore(el.$fixedEl)
var $el = el.$fixedEl var fixedTop = $siblingEl.offset().top - $siblingEl.position().top
var $img = $el.find('img') $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) { fixedOffset = isNaN(fixedOffset) ? 0 : fixedOffset
var image = new global.Image()
image.onload = function () { if ($img.length > 0) {
el.height = $el.outerHeight() + fixedOffset var image = new global.Image()
} image.onload = function () {
image.src = $img.attr('src')
} else {
el.height = $el.outerHeight() + fixedOffset el.height = $el.outerHeight() + fixedOffset
el.scrolledTo = self.getScrollingTo(el)
} }
}, image.src = $img.attr('src')
init: function () { } else {
var $els = $('.js-stick-at-top-when-scrolling') 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) { if ($els.length > 0) {
$els.each(function (i, el) { $els.each(function (i, el) {
var $el = $(el) var $el = $(el)
var el = { var el = {
$fixedEl: $el $fixedEl: $el,
} scrolledFrom: self.getScrolledFrom($el),
stopped: false
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 (sticky._resizeTimeout === false) { self.setFixedTop(el)
$(global).resize(sticky.onResize) self.setElHeight(el)
sticky._resizeTimeout = global.setInterval(sticky.checkResize, 50) 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 if (self._resizeTimeout === false) {
}, $(global).resize(function (e) { self.onResize() })
checkScroll: function () { self._resizeTimeout = global.setInterval(function (e) { self.checkResize() }, 50)
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('<div class="shim" style="width: ' + width + 'px; height: ' + height + 'px">&nbsp;</div>')
},
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()
} }
} }
} }
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('<div class="shim" style="width: ' + width + 'px height: ' + height + 'px">&nbsp</div>')
}
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('<div class="shim" style="width: ' + width + 'px height: ' + height + 'px">&nbsp</div>')
}
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 global.GOVUK = GOVUK
})(window) })(window)

View File

@@ -1,16 +1,13 @@
// CSS adapted from // CSS adapted from
// https://github.com/alphagov/govuk_frontend_toolkit/blob/d9489a987086471fe30b4b925a81c12cd198c91d/docs/javascript.md#stick-at-top-when-scrolling // 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; overflow: hidden;
margin-left: -$gutter-half; margin-left: -$gutter-half;
margin-top: -10px;
margin-bottom: 5px;
padding: 10px 0 0 $gutter-half; padding: 10px 0 0 $gutter-half;
position: relative; position: relative;
top: 5px;
transition: top 0.1s ease-out, box-shadow 1s ease-in-out;
.form-group { .form-group {
margin-bottom: 20px; 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 { .content-fixed {
position: fixed; position: fixed;
top: 0;
background: $white; background: $white;
z-index: 100; z-index: 100;
margin-top: 0;
padding-right: $gutter-half; 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 { .back-to-top-link {
opacity: 1; 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 { .shim {
display: block; display: block;
margin-bottom: 5px; margin-bottom: 5px;