417 lines
11 KiB
JavaScript
417 lines
11 KiB
JavaScript
(function (global, factory) {
|
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
|
typeof define === 'function' && define.amd ? define(factory) :
|
|
(global = global || self, global.scrollama = factory());
|
|
}(this, (function () { 'use strict';
|
|
|
|
// DOM helper functions
|
|
|
|
// public
|
|
function selectAll(selector, parent = document) {
|
|
if (typeof selector === 'string') {
|
|
return Array.from(parent.querySelectorAll(selector));
|
|
} else if (selector instanceof Element) {
|
|
return [selector];
|
|
} else if (selector instanceof NodeList) {
|
|
return Array.from(selector);
|
|
} else if (selector instanceof Array) {
|
|
return selector;
|
|
}
|
|
return [];
|
|
}
|
|
|
|
// SETUP
|
|
function create(className) {
|
|
const el = document.createElement("div");
|
|
el.className = `scrollama__debug-step ${className}`;
|
|
el.style.position = "fixed";
|
|
el.style.left = "0";
|
|
el.style.width = "100%";
|
|
el.style.zIndex = "9999";
|
|
el.style.borderTop = "2px solid black";
|
|
el.style.borderBottom = "2px solid black";
|
|
|
|
const p = document.createElement("p");
|
|
p.style.position = "absolute";
|
|
p.style.left = "0";
|
|
p.style.height = "1px";
|
|
p.style.width = "100%";
|
|
p.style.borderTop = "1px dashed black";
|
|
|
|
el.appendChild(p);
|
|
document.body.appendChild(el);
|
|
return el;
|
|
}
|
|
|
|
// UPDATE
|
|
function update({ id, step, marginTop }) {
|
|
const { index, height } = step;
|
|
const className = `scrollama__debug-step--${id}-${index}`;
|
|
let el = document.querySelector(`.${className}`);
|
|
if (!el) el = create(className);
|
|
|
|
el.style.top = `${marginTop * -1}px`;
|
|
el.style.height = `${height}px`;
|
|
el.querySelector("p").style.top = `${height / 2}px`;
|
|
}
|
|
|
|
function generateId() {
|
|
const alphabet = "abcdefghijklmnopqrstuvwxyz";
|
|
const date = Date.now();
|
|
const result = [];
|
|
for (let i = 0; i < 6; i += 1) {
|
|
const char = alphabet[Math.floor(Math.random() * alphabet.length)];
|
|
result.push(char);
|
|
}
|
|
return `${result.join("")}${date}`;
|
|
}
|
|
|
|
function err$1(msg) {
|
|
console.error(`scrollama error: ${msg}`);
|
|
}
|
|
|
|
function getIndex(node) {
|
|
return +node.getAttribute("data-scrollama-index");
|
|
}
|
|
|
|
function createProgressThreshold(height, threshold) {
|
|
const count = Math.ceil(height / threshold);
|
|
const t = [];
|
|
const ratio = 1 / count;
|
|
for (let i = 0; i < count + 1; i += 1) {
|
|
t.push(i * ratio);
|
|
}
|
|
return t;
|
|
}
|
|
|
|
function parseOffset(x) {
|
|
if (typeof x === "string" && x.indexOf("px") > 0) {
|
|
const v = +x.replace("px", "");
|
|
if (!isNaN(v)) return { format: "pixels", value: v };
|
|
else {
|
|
err("offset value must be in 'px' format. Fallback to 0.5.");
|
|
return { format: "percent", value: 0.5 };
|
|
}
|
|
} else if (typeof x === "number" || !isNaN(+x)) {
|
|
if (x > 1) err("offset value is greater than 1. Fallback to 1.");
|
|
if (x < 0) err("offset value is lower than 0. Fallback to 0.");
|
|
return { format: "percent", value: Math.min(Math.max(0, x), 1) };
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function indexSteps(steps) {
|
|
steps.forEach((step) =>
|
|
step.node.setAttribute("data-scrollama-index", step.index)
|
|
);
|
|
}
|
|
|
|
function getOffsetTop(node) {
|
|
const { top } = node.getBoundingClientRect();
|
|
const scrollTop = window.pageYOffset;
|
|
const clientTop = document.body.clientTop || 0;
|
|
return top + scrollTop - clientTop;
|
|
}
|
|
|
|
let currentScrollY;
|
|
let comparisonScrollY;
|
|
let direction;
|
|
|
|
function onScroll(container) {
|
|
const scrollTop = container ? container.scrollTop : window.pageYOffset;
|
|
|
|
if (currentScrollY === scrollTop) return;
|
|
currentScrollY = scrollTop;
|
|
if (currentScrollY > comparisonScrollY) direction = "down";
|
|
else if (currentScrollY < comparisonScrollY) direction = "up";
|
|
comparisonScrollY = currentScrollY;
|
|
}
|
|
|
|
function setupScroll(container) {
|
|
currentScrollY = 0;
|
|
comparisonScrollY = 0;
|
|
document.addEventListener("scroll", () => onScroll(container));
|
|
}
|
|
|
|
function scrollama() {
|
|
let cb = {};
|
|
|
|
let id = generateId();
|
|
let steps = [];
|
|
let globalOffset;
|
|
let containerElement;
|
|
let rootElement;
|
|
|
|
let progressThreshold = 0;
|
|
|
|
let isEnabled = false;
|
|
let isProgress = false;
|
|
let isDebug = false;
|
|
let isTriggerOnce = false;
|
|
|
|
let exclude = [];
|
|
|
|
/* HELPERS */
|
|
function reset() {
|
|
cb = {
|
|
stepEnter: () => { },
|
|
stepExit: () => { },
|
|
stepProgress: () => { },
|
|
};
|
|
exclude = [];
|
|
}
|
|
|
|
function handleEnable(shouldEnable) {
|
|
if (shouldEnable && !isEnabled) updateObservers();
|
|
if (!shouldEnable && isEnabled) disconnectObservers();
|
|
isEnabled = shouldEnable;
|
|
}
|
|
|
|
/* NOTIFY CALLBACKS */
|
|
function notifyProgress(element, progress) {
|
|
const index = getIndex(element);
|
|
const step = steps[index];
|
|
if (progress !== undefined) step.progress = progress;
|
|
const response = { element, index, progress, direction };
|
|
if (step.state === "enter") cb.stepProgress(response);
|
|
}
|
|
|
|
function notifyStepEnter(element, check = true) {
|
|
const index = getIndex(element);
|
|
const step = steps[index];
|
|
const response = { element, index, direction };
|
|
|
|
step.direction = direction;
|
|
step.state = "enter";
|
|
|
|
// if (isPreserveOrder && check && direction !== "up")
|
|
// notifyOthers(index, "above");
|
|
// if (isPreserveOrder && check && direction === "up")
|
|
// notifyOthers(index, "below");
|
|
|
|
if (!exclude[index]) cb.stepEnter(response);
|
|
if (isTriggerOnce) exclude[index] = true;
|
|
}
|
|
|
|
function notifyStepExit(element, check = true) {
|
|
const index = getIndex(element);
|
|
const step = steps[index];
|
|
|
|
if (!step.state) return false;
|
|
|
|
const response = { element, index, direction };
|
|
|
|
if (isProgress) {
|
|
if (direction === "down" && step.progress < 1) notifyProgress(element, 1);
|
|
else if (direction === "up" && step.progress > 0)
|
|
notifyProgress(element, 0);
|
|
}
|
|
|
|
step.direction = direction;
|
|
step.state = "exit";
|
|
|
|
cb.stepExit(response);
|
|
}
|
|
|
|
/* OBSERVERS - HANDLING */
|
|
function resizeStep([entry]) {
|
|
const index = getIndex(entry.target);
|
|
const step = steps[index];
|
|
const h = entry.target.offsetHeight;
|
|
if (h !== step.height) {
|
|
step.height = h;
|
|
disconnectObserver(step);
|
|
updateStepObserver(step);
|
|
updateResizeObserver(step);
|
|
}
|
|
}
|
|
|
|
function intersectStep([entry]) {
|
|
onScroll(containerElement);
|
|
|
|
const { isIntersecting, target } = entry;
|
|
if (isIntersecting) notifyStepEnter(target);
|
|
else notifyStepExit(target);
|
|
}
|
|
|
|
function intersectProgress([entry]) {
|
|
const index = getIndex(entry.target);
|
|
const step = steps[index];
|
|
const { isIntersecting, intersectionRatio, target } = entry;
|
|
if (isIntersecting && step.state === "enter")
|
|
notifyProgress(target, intersectionRatio);
|
|
}
|
|
|
|
/* OBSERVERS - CREATION */
|
|
function disconnectObserver({ observers }) {
|
|
Object.keys(observers).map((name) => {
|
|
observers[name].disconnect();
|
|
});
|
|
}
|
|
|
|
function disconnectObservers() {
|
|
steps.forEach(disconnectObserver);
|
|
}
|
|
|
|
function updateResizeObserver(step) {
|
|
const observer = new ResizeObserver(resizeStep);
|
|
observer.observe(step.node);
|
|
step.observers.resize = observer;
|
|
}
|
|
|
|
function updateResizeObservers() {
|
|
steps.forEach(updateResizeObserver);
|
|
}
|
|
|
|
function updateStepObserver(step) {
|
|
const h = window.innerHeight;
|
|
const off = step.offset || globalOffset;
|
|
const factor = off.format === "pixels" ? 1 : h;
|
|
const offset = off.value * factor;
|
|
const marginTop = step.height / 2 - offset;
|
|
const marginBottom = step.height / 2 - (h - offset);
|
|
const rootMargin = `${marginTop}px 0px ${marginBottom}px 0px`;
|
|
const root = rootElement;
|
|
|
|
const threshold = 0.5;
|
|
const options = { rootMargin, threshold, root };
|
|
const observer = new IntersectionObserver(intersectStep, options);
|
|
|
|
observer.observe(step.node);
|
|
step.observers.step = observer;
|
|
|
|
if (isDebug) update({ id, step, marginTop, marginBottom });
|
|
}
|
|
|
|
function updateStepObservers() {
|
|
steps.forEach(updateStepObserver);
|
|
}
|
|
|
|
function updateProgressObserver(step) {
|
|
const h = window.innerHeight;
|
|
const off = step.offset || globalOffset;
|
|
const factor = off.format === "pixels" ? 1 : h;
|
|
const offset = off.value * factor;
|
|
const marginTop = -offset + step.height;
|
|
const marginBottom = offset - h;
|
|
const rootMargin = `${marginTop}px 0px ${marginBottom}px 0px`;
|
|
|
|
const threshold = createProgressThreshold(step.height, progressThreshold);
|
|
const options = { rootMargin, threshold };
|
|
const observer = new IntersectionObserver(intersectProgress, options);
|
|
|
|
observer.observe(step.node);
|
|
step.observers.progress = observer;
|
|
}
|
|
|
|
function updateProgressObservers() {
|
|
steps.forEach(updateProgressObserver);
|
|
}
|
|
|
|
function updateObservers() {
|
|
disconnectObservers();
|
|
updateResizeObservers();
|
|
updateStepObservers();
|
|
if (isProgress) updateProgressObservers();
|
|
}
|
|
|
|
/* SETUP */
|
|
const S = {};
|
|
|
|
S.setup = ({
|
|
step,
|
|
parent,
|
|
offset = 0.5,
|
|
threshold = 4,
|
|
progress = false,
|
|
once = false,
|
|
debug = false,
|
|
container = undefined,
|
|
root = null
|
|
}) => {
|
|
|
|
setupScroll(container);
|
|
|
|
steps = selectAll(step, parent).map((node, index) => ({
|
|
index,
|
|
direction: undefined,
|
|
height: node.offsetHeight,
|
|
node,
|
|
observers: {},
|
|
offset: parseOffset(node.dataset.offset),
|
|
top: getOffsetTop(node),
|
|
progress: 0,
|
|
state: undefined,
|
|
}));
|
|
|
|
if (!steps.length) {
|
|
err$1("no step elements");
|
|
return S;
|
|
}
|
|
|
|
isProgress = progress;
|
|
isTriggerOnce = once;
|
|
isDebug = debug;
|
|
progressThreshold = Math.max(1, +threshold);
|
|
globalOffset = parseOffset(offset);
|
|
containerElement = container;
|
|
rootElement = root;
|
|
|
|
reset();
|
|
indexSteps(steps);
|
|
handleEnable(true);
|
|
return S;
|
|
};
|
|
|
|
S.enable = () => {
|
|
handleEnable(true);
|
|
return S;
|
|
};
|
|
|
|
S.disable = () => {
|
|
handleEnable(false);
|
|
return S;
|
|
};
|
|
|
|
S.destroy = () => {
|
|
handleEnable(false);
|
|
reset();
|
|
return S;
|
|
};
|
|
|
|
S.resize = () => {
|
|
updateObservers();
|
|
return S;
|
|
};
|
|
|
|
S.offset = (x) => {
|
|
if (x === null || x === undefined) return globalOffset.value;
|
|
globalOffset = parseOffset(x);
|
|
updateObservers();
|
|
return S;
|
|
};
|
|
|
|
S.onStepEnter = (f) => {
|
|
if (typeof f === "function") cb.stepEnter = f;
|
|
else err$1("onStepEnter requires a function");
|
|
return S;
|
|
};
|
|
|
|
S.onStepExit = (f) => {
|
|
if (typeof f === "function") cb.stepExit = f;
|
|
else err$1("onStepExit requires a function");
|
|
return S;
|
|
};
|
|
|
|
S.onStepProgress = (f) => {
|
|
if (typeof f === "function") cb.stepProgress = f;
|
|
else err$1("onStepProgress requires a function");
|
|
return S;
|
|
};
|
|
return S;
|
|
}
|
|
|
|
return scrollama;
|
|
|
|
})));
|