Skip to content

Instantly share code, notes, and snippets.

@zengabor
Last active February 25, 2018 21:01
Show Gist options
  • Save zengabor/aa87f9c1e37176c7b9480ca35f7fb940 to your computer and use it in GitHub Desktop.
Save zengabor/aa87f9c1e37176c7b9480ca35f7fb940 to your computer and use it in GitHub Desktop.
Zenspot v1.1 - Execute something when an element is spotted (becomes visible)
/**
* Zenspot v1.1 - Execute something when an element is spotted (becomes visible)
*
* Copyright 2017-2018 Gabor Lenard, https://github.com/zengabor
*
* This is free and unencumbered software released into the public domain.
*
* Anyone is free to copy, modify, publish, use, compile, sell, or
* distribute this software, either in source code form or as a compiled
* binary, for any purpose, commercial or non-commercial, and by any
* means.
*
* In jurisdictions that recognize copyright laws, the author or authors
* of this software dedicate any and all copyright interest in the
* software to the public domain. We make this dedication for the benefit
* of the public at large and to the detriment of our heirs and
* successors. We intend this dedication to be an overt act of
* relinquishment in perpetuity of all present and future rights to this
* software under copyright law.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
* OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* For more information, please refer to <http://unlicense.org>
*
*/
/*jshint devel:true, asi:true */
/*global define, module */
(function (root, factory) {
if (typeof define === "function" && define.amd) {
define([], factory(root))
} else if (typeof module === "object" && module.exports) {
module.exports = factory(root)
} else {
root.Zenspot = factory(root)
}
}(this, function (win) {
"use strict"
if (typeof window === "undefined" || !("document" in window)) {
return {}
}
// The following two variables can be set via Zenspot.config()
// The scroll is ignored if the user scrolls less then this amount of pixels:
var minimumDelta = 10 // pixels
// How often the checks should me run
var frequency = 32 // ms, which will result in a frequency around 30fps
var onTopCallbacks = []
var onBottomCallbacks = []
var onUpCallbacks = []
var onDownCallbacks = []
var toBeWatched = []
var lastY = 0
var hasScrolled
var didScroll = function () { hasScrolled = true }
var hasResized
var didResize = function () { hasResized = true }
var initOnce = function () {
initOnce = function () {}
_init()
}
var _init = function () {
win.addEventListener("scroll", didScroll, false)
win.addEventListener("resize", didResize, false)
win.addEventListener("orientationchange", didResize, false);
(function pollForChanges() {
setTimeout(function () {
if (hasScrolled) {
hasScrolled = false
handleScroll()
goThroughTheWatchList()
}
if (hasResized) {
hasResized = false
goThroughTheWatchList()
}
// is there anything left to watch?
if (toBeWatched.length || onTopCallbacks.length || onBottomCallbacks.length || onUpCallbacks.length || onDownCallbacks.length) {
pollForChanges()
} else {
win.removeEventListener("scroll", didScroll, false)
win.removeEventListener("resize", didResize, false)
win.removeEventListener("orientationchange", didResize, false)
initOnce = _init
}
}, frequency)
})()
}
var callAsync = function (callback) { setTimeout(callback, 1) }
var callAll = function (handlers) {
for (var i = 0, l = handlers.length; i < l; i++) {
callAsync(handlers[i])
}
}
var handleScroll = function () {
var y = getY()
var isDownwards = y > lastY && (y - lastY > minimumDelta)
var isUpwards = y < lastY && (lastY - y > minimumDelta)
if (!(isDownwards || isUpwards)) {
return
}
lastY = y
if (isDownwards) {
callAll(onDownCallbacks)
} else {
callAll(onUpCallbacks)
}
if (y < minimumDelta) {
callAll(onTopCallbacks)
}
if (y + getViewHeight() >= (document.height || document.body.clientHeight)) {
callAll(onBottomCallbacks)
}
}
var goThroughTheWatchList = function () {
var i = toBeWatched.length
while (i--) {
var remove = function () { toBeWatched.splice(i, 1) }
var item = toBeWatched[i]
if (!document.body.contains(item.elem)) { // Is it still part of the DOM?
remove()
} else {
var visibility = getVisibility(item.elem, item.visibilityA, item.visibilityB)
if (visibility.visible) {
if (item.onlyOnce) {
remove()
}
callAsync(item.handler.bind(item.elem, visibility.percent))
}
}
}
}
var getVisibility = function (elem, minVisibilityPercentFromAbove, minVisibilityPercentFromBelow) {
var bcr = elem.getBoundingClientRect()
var viewTop = getY()
var elemTop = bcr.top + viewTop - document.documentElement.offsetTop
var elemHeight = bcr.height
var elemBottom = elemTop + elemHeight
var viewHeight = getViewHeight()
var viewBottom = viewTop + viewHeight
if (elemBottom < viewTop || elemTop > viewBottom) {
return { visible: false, percent: 0.0 }
} else if (elemTop >= viewTop && elemBottom <= viewBottom) {
return { visible: true, percent: 1.0 }
} else {
var minHeight = Math.min(elemHeight, viewHeight)
var visibilityFromAbove = (viewBottom - elemTop) / minHeight
var visibilityFromBelow = (elemBottom - viewTop) / minHeight
var visibilityPercentage = Math.min(visibilityFromAbove, visibilityFromBelow)
if (elemTop >= viewTop) {
return { visible: visibilityFromAbove > minVisibilityPercentFromAbove, percent: visibilityPercentage }
} else {
return { visible: visibilityFromBelow > minVisibilityPercentFromBelow, percent: visibilityPercentage }
}
}
}
var getY = function () { return win.scrollY || document.documentElement.scrollTop }
var getViewHeight = function () { return win.innerHeight || document.documentElement.clientHeight }
var addToWatchList = function (elem, onSpotted, visibilityPercentFromAbove, visibilityPercentFromBelow, onlyOnce) {
var visibilityA = Math.min(1, visibilityPercentFromAbove || 0.5)
var visibilityB = Math.min(1, visibilityPercentFromBelow || 1)
var initialVisibility = getVisibility(elem, visibilityA, visibilityB)
if (initialVisibility.visible) {
onSpotted.call(elem, initialVisibility.percent)
if (onlyOnce) {
return
}
}
toBeWatched.push({
elem: elem,
handler: onSpotted,
visibilityA: visibilityA,
visibilityB: visibilityB,
onlyOnce: !!onlyOnce
})
initOnce()
}
// IMPORTANT: these functions should be only called after the page load event.
return {
config: function (newFrequency, newMinimumDelta) {
if (newFrequency) {
frequency = newFrequency
}
if (newMinimumDelta) {
minimumDelta = newMinimumDelta
}
return {
frequency: frequency,
minimumDelta: minimumDelta
}
},
watch: function (elem, onSpotted, visibilityPercentFromAbove, visibilityPercentFromBelow) {
addToWatchList(elem, onSpotted, visibilityPercentFromAbove, visibilityPercentFromBelow)
},
watchOnce: function (elem, onSpotted, visibilityPercentFromAbove, visibilityPercentFromBelow) {
addToWatchList(elem, onSpotted, visibilityPercentFromAbove, visibilityPercentFromBelow, true)
},
// clear: function () { toBeWatched = [] },
onTop: function (onArrivingToTop) { onTopCallbacks.push(onArrivingToTop); initOnce() },
onBottom: function (onArrivingToBottom) { onBottomCallbacks.push(onArrivingToBottom); initOnce() },
onScrollUp: function (onScrollingUpwards) { onUpCallbacks.push(onScrollingUpwards); initOnce() },
onScrollDown: function (onScrollingDownwards) { onDownCallbacks.push(onScrollingDownwards); initOnce() }
}
}));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment