Skip to content

Instantly share code, notes, and snippets.

@Extrapolator214
Last active November 27, 2015 14:30
Show Gist options
  • Save Extrapolator214/dfd852bf248a0df0ce18 to your computer and use it in GitHub Desktop.
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
# 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