Last active
November 27, 2015 14:30
-
-
Save Extrapolator214/dfd852bf248a0df0ce18 to your computer and use it in GitHub Desktop.
plugin to make fixed elements (like sidebar or top nav bar) scrollable. Unfinished, needs performance and style improvements
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# EXAMPLE USAGE | |
#sidebar.scrollFixed( | |
#conditions: | |
#top: -> | |
#nav.css('position') == 'fixed' and | |
#windowObj.height() > 300 and | |
#sidebar.outerHeight() < content.outerHeight() | |
#offset: | |
#top: 40 | |
#bottom: 0 | |
#collision: | |
#bottom: | |
#element: footer | |
#margin: 50 | |
#handler: -> | |
#sidebar.css | |
#position: 'absolute' | |
#top: '' | |
#bottom: footer.outerHeight() - 50 | |
#left: sidebarHomeX - leftMargin | |
#options: | |
#zIndex: 80 | |
#handlerForFixed: -> | |
#sidebar.css | |
#left: content.offset().left - $(window).scrollLeft() + content.width() + 10 | |
#nonScrollableAdjustment: | |
#bottom: 20 | |
#handlerForFixedResize: -> | |
#if sidebar.css('position') == 'fixed' | |
#sidebar.css | |
#left: content.offset().left - $(window).scrollLeft() + content.width() + 10 | |
#) | |
(($) -> | |
data = null | |
lastScrollLeft = {} | |
lastScrollTop = {} | |
$.fn.scrollFixed = (options) -> | |
data = $.extend(true, # merge recursively with defaults | |
offset: | |
top: 0 | |
bottom: 0 | |
left: 0 | |
right: 0 | |
conditions: | |
# functions which should return true/false, tested on every scroll event | |
top: -> false | |
bottom: -> false | |
left: -> false | |
right: -> false | |
collision: | |
# functions to handle interaction with other elements such as footer | |
top: {handler: null, element: null, margin: 0} | |
bottom: {handler: null, element: null, margin: 0} | |
left: {handler: null, element: null, margin: 0} | |
right: {handler: null, element: null, margin: 0} | |
options: | |
horizontal: true | |
vertical: true | |
zIndex: 100 | |
handlerForFixed: null # function | |
handlerForNonFixed: null # function | |
handlerForFixedResize: true # can be function or boolean | |
nonScrollableAdjustment: {top: 0, bottom: 0} | |
, options) | |
data.options.handlerForFixedResize = switch data.options.handlerForFixedResize | |
when true then data.options.handlerForFixed | |
when false then null | |
else data.options.handlerForFixedResize | |
data.elementHeight = $(this).height() | |
# workaround to track scroll direction on every element separately | |
namespace = this.selector.replace(/^\s+|\s+$/g, '').replace(/[^a-zA-Z0-9]+/g, '') | |
lastScrollTop[namespace] = 0 | |
lastScrollLeft[namespace] = 0 | |
$(window).off("scroll.#{namespace}", scrollHandler) | |
$(window).on("scroll.#{namespace}", {elements: $(this), settings: data}, scrollHandler) | |
if data.options.handlerForFixedResize? and data.options.handlerForFixedResize != false | |
$(window).off("resize.#{namespace}", data.options.handlerForFixedResize) | |
$(window).on("resize.#{namespace}", data.options.handlerForFixedResize) | |
this | |
# windowObj should be passed to improve performance | |
$.fn.isOnScreen = (side, margin = 0, windowObj = $(window)) -> | |
view = {} | |
bounds = {} | |
element = this | |
if side in ['top', 'bottom'] | |
view.top = windowObj.scrollTop() | |
view.bottom = view.top + windowObj.height() | |
bounds.top = element.offset().top | |
bounds.bottom = bounds.top + element.outerHeight() | |
else if side in ['left', 'right'] | |
view.left = windowObj.scrollLeft() | |
view.right = view.left + windowObj.width() | |
bounds.left = element.offset().left | |
bounds.right = bounds.left + element.outerWidth() | |
switch side | |
# margin shrinks or expands the bounds, not shifts them | |
when 'top' then bounds.top + margin <= view.bottom and bounds.top - margin >= view.top | |
when 'bottom' then bounds.bottom + margin >= view.top and bounds.bottom - margin <= view.bottom | |
when 'left' then bounds.left + margin >= view.right and bounds.left - margin <= view.left | |
when 'right' then bounds.right - margin >= view.left and bounds.right + margin <= view.right | |
else null | |
scrollHandler = (e) -> | |
settings = e.data.settings | |
fixAtTop = settings.conditions.top() | |
fixAtBottom = settings.conditions.bottom() | |
fixAtLeft = settings.conditions.left() | |
fixAtRight = settings.conditions.right() | |
shouldBeFixed = fixAtTop or fixAtBottom or fixAtLeft or fixAtRight | |
windowObj = $(window) | |
scrollLeft = windowObj.scrollLeft() | |
scrollTop = windowObj.scrollTop() | |
unless shouldBeFixed | |
e.data.elements.each -> | |
$(this).attr('style', '') | |
settings.options.handlerForNonFixed() if settings.options.handlerForNonFixed? | |
lastScrollTop[e.handleObj.namespace] = scrollTop | |
return | |
e.data.elements.each -> | |
# main logic | |
element = $(this) | |
return if element.is(':hidden') | |
downscroll = lastScrollTop[e.handleObj.namespace] < scrollTop | |
scrollHeight = scrollTop - lastScrollTop[e.handleObj.namespace] | |
bounds = element.get(0).getBoundingClientRect() | |
collideAtTop = settings.collision.top.element? and settings.collision.top.element.isOnScreen('bottom', settings.collision.top.margin, windowObj) and settings.collision.top.handler? | |
if settings.collision.bottom.element? and settings.collision.bottom.handler? | |
collideAtBottom = | |
settings.collision.bottom.element.isOnScreen('top', settings.collision.bottom.margin, windowObj) and | |
settings.collision.bottom.element.offset().top - settings.elementHeight - | |
parseInt(element.css('margin-top')) - | |
settings.collision.bottom.margin <= element.offset().top and | |
bounds.bottom - settings.collision.bottom.margin >= settings.collision.bottom.element.get(0).getBoundingClientRect().top and | |
(if !downscroll then bounds.top <= settings.offset.top + parseInt(element.css('margin-top')) else true) | |
else | |
collideAtBottom = false | |
collideAtLeft = settings.collision.left.element? and settings.collision.left.element.isOnScreen('right', settings.collision.left.margin, windowObj) and settings.collision.left.handler? | |
collideAtRight = settings.collision.right.element? and settings.collision.right.element.isOnScreen('left', settings.collision.right.margin, windowObj) and settings.collision.right.handler? | |
collision = collideAtTop or collideAtBottom or collideAtLeft or collideAtRight | |
if element.css('position') != 'fixed' and !collision | |
element.css | |
position: 'fixed' | |
'z-index': settings.options.zIndex if settings.options.zIndex? | |
top: settings.offset.top + scrollHeight if downscroll | |
bottom: settings.offset.bottom unless downscroll | |
# bounds should be obtained after element is fixed | |
bounds = element.get(0).getBoundingClientRect() | |
setHorizontalBounds = -> | |
left = if fixAtLeft then settings.offset.left else '' | |
right = if fixAtRight then settings.offset.right else '' | |
lastScrollLeft[e.handleObj.namespace] = scrollLeft | |
element.css | |
left: left | |
right: right | |
setVerticalBounds = -> | |
currentTop = bounds.top - parseInt(element.css('margin-top')) | |
top = currentTop - scrollHeight | |
# not scrollable | |
if bounds.bottom - scrollHeight + settings.offset.bottom - settings.options.nonScrollableAdjustment.bottom + settings.offset.top < windowObj.height() and currentTop >= settings.offset.top - settings.options.nonScrollableAdjustment.top | |
top = if fixAtTop then settings.offset.top else '' | |
bottom = if fixAtBottom then settings.offset.bottom else '' | |
# fix at the top of the viewport | |
else if top >= settings.offset.top and !downscroll | |
top = settings.offset.top | |
bottom = '' | |
# fix at the bottom of the viewport | |
else if bounds.bottom - scrollHeight <= windowObj.height() and downscroll | |
top = '' | |
bottom = settings.offset.bottom | |
# move in the direction of the scroll | |
else | |
bottom = '' | |
if top > settings.elementHeight or top < -settings.elementHeight | |
if downscroll | |
# default bottom | |
top = '' | |
bottom = settings.offset.bottom | |
else | |
# default top | |
top = settings.offset.top | |
bottom = '' | |
element.css | |
top: top | |
bottom: bottom | |
# horizontal scroll | |
if lastScrollLeft[e.handleObj.namespace] != scrollLeft | |
if settings.options.horizontal | |
if collideAtLeft or collideAtRight | |
setVerticalBounds() | |
settings.collision.left.handler() if collideAtLeft | |
settings.collision.right.handler() if collideAtRight | |
collision = true | |
else | |
collision = false | |
setHorizontalBounds() | |
# vertical scroll | |
else if settings.options.vertical | |
if collideAtBottom or collideAtTop | |
setHorizontalBounds() | |
settings.collision.bottom.handler() if collideAtBottom | |
settings.collision.top.handler() if collideAtTop | |
collision = true | |
else | |
collision = false | |
setVerticalBounds() | |
settings.options.handlerForFixed() if settings.options.handlerForFixed? and !collision | |
lastScrollTop[e.handleObj.namespace] = scrollTop | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment