flipflop-scroll
Version:
A tiny vanilla JS library to scroll section by section
838 lines (727 loc) • 25.9 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.FlipflopScroll = factory());
}(this, (function () { 'use strict';
function noop() { }
function assign(tar, src) {
// @ts-ignore
for (const k in src)
tar[k] = src[k];
return tar;
}
function run(fn) {
return fn();
}
function blank_object() {
return Object.create(null);
}
function run_all(fns) {
fns.forEach(run);
}
function is_function(thing) {
return typeof thing === 'function';
}
function safe_not_equal(a, b) {
return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function');
}
function is_empty(obj) {
return Object.keys(obj).length === 0;
}
function create_slot(definition, ctx, $$scope, fn) {
if (definition) {
const slot_ctx = get_slot_context(definition, ctx, $$scope, fn);
return definition[0](slot_ctx);
}
}
function get_slot_context(definition, ctx, $$scope, fn) {
return definition[1] && fn
? assign($$scope.ctx.slice(), definition[1](fn(ctx)))
: $$scope.ctx;
}
function get_slot_changes(definition, $$scope, dirty, fn) {
if (definition[2] && fn) {
const lets = definition[2](fn(dirty));
if ($$scope.dirty === undefined) {
return lets;
}
if (typeof lets === 'object') {
const merged = [];
const len = Math.max($$scope.dirty.length, lets.length);
for (let i = 0; i < len; i += 1) {
merged[i] = $$scope.dirty[i] | lets[i];
}
return merged;
}
return $$scope.dirty | lets;
}
return $$scope.dirty;
}
function update_slot(slot, slot_definition, ctx, $$scope, dirty, get_slot_changes_fn, get_slot_context_fn) {
const slot_changes = get_slot_changes(slot_definition, $$scope, dirty, get_slot_changes_fn);
if (slot_changes) {
const slot_context = get_slot_context(slot_definition, ctx, $$scope, get_slot_context_fn);
slot.p(slot_context, slot_changes);
}
}
function append(target, node) {
target.appendChild(node);
}
function detach(node) {
node.parentNode.removeChild(node);
}
function element(name) {
return document.createElement(name);
}
function children(element) {
return Array.from(element.childNodes);
}
let current_component;
function set_current_component(component) {
current_component = component;
}
const dirty_components = [];
const binding_callbacks = [];
const render_callbacks = [];
const flush_callbacks = [];
const resolved_promise = Promise.resolve();
let update_scheduled = false;
function schedule_update() {
if (!update_scheduled) {
update_scheduled = true;
resolved_promise.then(flush);
}
}
function add_render_callback(fn) {
render_callbacks.push(fn);
}
let flushing = false;
const seen_callbacks = new Set();
function flush() {
if (flushing)
return;
flushing = true;
do {
// first, call beforeUpdate functions
// and update components
for (let i = 0; i < dirty_components.length; i += 1) {
const component = dirty_components[i];
set_current_component(component);
update(component.$$);
}
dirty_components.length = 0;
while (binding_callbacks.length)
binding_callbacks.pop()();
// then, once components are updated, call
// afterUpdate functions. This may cause
// subsequent updates...
for (let i = 0; i < render_callbacks.length; i += 1) {
const callback = render_callbacks[i];
if (!seen_callbacks.has(callback)) {
// ...so guard against infinite loops
seen_callbacks.add(callback);
callback();
}
}
render_callbacks.length = 0;
} while (dirty_components.length);
while (flush_callbacks.length) {
flush_callbacks.pop()();
}
update_scheduled = false;
flushing = false;
seen_callbacks.clear();
}
function update($$) {
if ($$.fragment !== null) {
$$.update();
run_all($$.before_update);
const dirty = $$.dirty;
$$.dirty = [-1];
$$.fragment && $$.fragment.p($$.ctx, dirty);
$$.after_update.forEach(add_render_callback);
}
}
const outroing = new Set();
let outros;
function transition_in(block, local) {
if (block && block.i) {
outroing.delete(block);
block.i(local);
}
}
function transition_out(block, local, detach, callback) {
if (block && block.o) {
if (outroing.has(block))
return;
outroing.add(block);
outros.c.push(() => {
outroing.delete(block);
if (callback) {
if (detach)
block.d(1);
callback();
}
});
block.o(local);
}
}
function mount_component(component, target, anchor) {
const { fragment, on_mount, on_destroy, after_update } = component.$$;
fragment && fragment.m(target, anchor);
// onMount happens before the initial afterUpdate
add_render_callback(() => {
const new_on_destroy = on_mount.map(run).filter(is_function);
if (on_destroy) {
on_destroy.push(...new_on_destroy);
}
else {
// Edge case - component was destroyed immediately,
// most likely as a result of a binding initialising
run_all(new_on_destroy);
}
component.$$.on_mount = [];
});
after_update.forEach(add_render_callback);
}
function destroy_component(component, detaching) {
const $$ = component.$$;
if ($$.fragment !== null) {
run_all($$.on_destroy);
$$.fragment && $$.fragment.d(detaching);
// TODO null out other refs, including component.$$ (but need to
// preserve final state?)
$$.on_destroy = $$.fragment = null;
$$.ctx = [];
}
}
function make_dirty(component, i) {
if (component.$$.dirty[0] === -1) {
dirty_components.push(component);
schedule_update();
component.$$.dirty.fill(0);
}
component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31));
}
function init(component, options, instance, create_fragment, not_equal, props, dirty = [-1]) {
const parent_component = current_component;
set_current_component(component);
const prop_values = options.props || {};
const $$ = component.$$ = {
fragment: null,
ctx: null,
// state
props,
update: noop,
not_equal,
bound: blank_object(),
// lifecycle
on_mount: [],
on_destroy: [],
before_update: [],
after_update: [],
context: new Map(parent_component ? parent_component.$$.context : []),
// everything else
callbacks: blank_object(),
dirty,
skip_bound: false
};
let ready = false;
$$.ctx = instance
? instance(component, prop_values, (i, ret, ...rest) => {
const value = rest.length ? rest[0] : ret;
if ($$.ctx && not_equal($$.ctx[i], $$.ctx[i] = value)) {
if (!$$.skip_bound && $$.bound[i])
$$.bound[i](value);
if (ready)
make_dirty(component, i);
}
return ret;
})
: [];
$$.update();
ready = true;
run_all($$.before_update);
// `false` as a special case of no DOM component
$$.fragment = create_fragment ? create_fragment($$.ctx) : false;
if (options.target) {
if (options.hydrate) {
const nodes = children(options.target);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
$$.fragment && $$.fragment.l(nodes);
nodes.forEach(detach);
}
else {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
$$.fragment && $$.fragment.c();
}
if (options.intro)
transition_in(component.$$.fragment);
mount_component(component, options.target, options.anchor);
flush();
}
set_current_component(parent_component);
}
class SvelteComponent {
$destroy() {
destroy_component(this, 1);
this.$destroy = noop;
}
$on(type, callback) {
const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));
callbacks.push(callback);
return () => {
const index = callbacks.indexOf(callback);
if (index !== -1)
callbacks.splice(index, 1);
};
}
$set($$props) {
if (this.$$set && !is_empty($$props)) {
this.$$.skip_bound = true;
this.$$set($$props);
this.$$.skip_bound = false;
}
}
}
function flipFlop(useOptions) {
useOptions = useOptions || {};
var defaultOptions = {
container: document.body,
sections: document.getElementsByTagName('section'),
nav: document.getElementById('flip_flop_nav'),
mouseDrag: false,
disableOnMax: 960
};
if (document.documentMode) {
if (!Object.assign) {
Object.defineProperty(Object, 'assign', {
enumerable: false,
configurable: true,
writable: true,
value: function (target) {
if (target === undefined || target === null) {
throw new TypeError('Cannot convert first argument to object');
}
var to = Object(target);
for (var i = 1; i < arguments.length; i++) {
var nextSource = arguments[i];
if (nextSource === undefined || nextSource === null) {
continue;
}
nextSource = Object(nextSource);
var keysArray = Object.keys(Object(nextSource));
for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
var nextKey = keysArray[nextIndex];
var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);
if (desc !== undefined && desc.enumerable) {
to[nextKey] = nextSource[nextKey];
}
}
}
return to;
}
});
}
}
var options = Object.assign({}, defaultOptions, useOptions);
var scrollTop,
sectionPositions = [],
appendDot = '<div class="dot"></div>',
dotCount = '',
navDots,
currentSection,
sectionHeight,
halfSection,
isDown = false,
startScroll,
scrollDown,
isScrollUp,
time,
wait = false,
allLinks;
function refreshSectionPositions() {
sectionPositions = [];
scrollTop = -options.sections[0].getBoundingClientRect().top;
var i;
for (i = 0; i < options.sections.length; i++) {
sectionPositions.push(options.sections[i].getBoundingClientRect().top + scrollTop);
}
}
function getPositionIndex() {
sectionHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
halfSection = sectionHeight / 2;
scrollTop = -options.sections[0].getBoundingClientRect().top;
for (var i = 0; i < options.sections.length; i++) {
if (sectionPositions[i] + halfSection > scrollTop && sectionPositions[i] - halfSection < scrollTop) {
currentSection = i;
}
}
}
function getDots() {
if (options.nav) {
navDots = options.nav.getElementsByClassName('dot');
if (navDots.length == 0) {
var i;
for (i = 0; i < options.sections.length; i++) {
dotCount = dotCount + appendDot;
}
options.nav.innerHTML = dotCount;
navDots = options.nav.getElementsByClassName('dot');
}
}
}
function setDotNavigation() {
if (navDots) {
if (navDots.length > 0 && currentSection <= navDots.length - 1 && !navDots[currentSection].classList.contains('on')) {
for (var i = 0; i < navDots.length; i++) {
navDots[i].classList.remove('on');
}
getPositionIndex();
navDots[currentSection].classList.add('on');
} else if (navDots.length > 0 && currentSection > navDots.length - 1 && navDots[(navDots.length - 1)].classList.contains('on')) {
navDots[(navDots.length - 1)].classList.remove('on');
}
}
}
function onWindowResize() {
refreshSectionPositions();
getPositionIndex();
setDotNavigation();
scrollToY(sectionPositions[currentSection], 300, 'easeOutQuint');
if (!iOS()) {
try {
// try to use localStorage
sessionStorage.setItem('saveBodyTop', sectionPositions[currentSection]);
} catch (e) {
// there was an error so...
console.log('local storage is unavailable');
}
}
}
// http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/ ====
window.requestAnimFrame = (function () {
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function (callback) {
window.setTimeout(callback, 1000 / 60);
};
})();
function scrollToY(scrollTargetY, speed, easing) {
var scrollY,
scrollTargetY = scrollTargetY || 0,
speed = speed || 2000,
easing = easing || 'easeOutSine',
currentTime = 0;
scrollY = -options.sections[0].getBoundingClientRect().top;
// min time .1, max time .8 seconds
time = Math.max(.1, Math.min(Math.abs(scrollY - scrollTargetY) / speed, .8));
// // add animation loop
function tick() {
currentTime += 1 / 60;
var p = currentTime / time,
t = easingEquations[easing](p);
if (p < 1) {
requestAnimFrame(tick);
options.container.scrollTop = scrollY + ((scrollTargetY - scrollY) * t);
} else {
options.container.scrollTop = scrollTargetY;
}
getPositionIndex();
setDotNavigation();
}
// call it once to get started
tick();
if (!iOS()) {
try {
// try to use localStorage
sessionStorage.setItem('saveBodyTop', sectionPositions[currentSection]);
} catch (e) {
// there was an error so...
console.log('local storage is unavailable');
}
}
}
// easing equations from https://github.com/danro/easing-js/blob/master/easing.js
var easingEquations = {
easeOutSine: function (pos) {
return Math.sin(pos * (Math.PI / 2));
},
easeInOutSine: function (pos) {
return (-0.5 * (Math.cos(Math.PI * pos) - 1));
},
easeOutQuint: function (pos) {
return (Math.pow((pos - 1), 5) + 1);
},
easeInOutQuint: function (pos) {
if ((pos /= 0.5) < 1) {
return 0.5 * Math.pow(pos, 5);
}
return 0.5 * (Math.pow((pos - 2), 5) + 2);
}
};
function debounce(func, wait, immediate) {
var timeout;
return function () {
var context = this, args = arguments;
var later = function () {
timeout = null;
if (!immediate) func.apply(context, args);
};
var callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) func.apply(context, args);
};
}
/* =================== CHECK FOR CSS SNAP POINTS =================== */
// mouse wheel only
function runOnScroll(event) {
if (options.disableOnMax <= Math.max(document.documentElement.clientWidth, window.innerWidth || 0)) {
if (document.documentMode) {
if (event.wheelDelta > 0) {
isScrollUp = true;
}
else {
isScrollUp = false;
}
} else {
if (event.deltaY > 0) {
isScrollUp = false;
}
else {
isScrollUp = true;
}
}
getPositionIndex();
var newSection;
if (isScrollUp) {
newSection = currentSection < 1 ? 0 : currentSection - 1;
// fix for scroll up jumping to bottom of new section (which is top)
if (newSection === currentSection) {
return;
} else if (scrollTop <= sectionPositions[currentSection] + 100) {
scrollToY((sectionPositions[newSection + 1] - sectionHeight), 300, 'easeOutQuint');
currentSection = newSection;
}
} else {
newSection = currentSection > options.sections.length - 2 ? options.sections.length - 1 : currentSection + 1;
if (scrollTop >= (sectionPositions[currentSection + 1] - sectionHeight)) {
scrollToY(sectionPositions[newSection], 300, 'easeOutQuint');
currentSection = newSection;
}
}
this.oldScroll = this.scrollY;
}
}
var debounceImmediately = debounce(runOnScroll, 250, true);
window.addEventListener('onwheel' in document ? 'wheel' : 'onmousewheel' in document ? 'mousewheel' : 'DOMMouseScroll', debounceImmediately, { passive: true });
// }
/* =================== END CSS SNAP POINTS =================== */
function mouseGrab() {
if (options.mouseGrab) {
options.container.addEventListener('mousedown', function (e) {
isDown = true;
startScroll = e.pageY;
scrollDown = -options.sections[0].getBoundingClientRect().top;
});
options.container.addEventListener('mouseleave', function () {
isDown = false;
setDotNavigation();
if (!iOS()) {
try {
// try to use localStorage
sessionStorage.setItem('saveBodyTop', sectionPositions[currentSection]);
} catch (e) {
// there was an error so...
console.log('local storage is unavailable');
}
}
});
options.container.addEventListener('mouseup', function (e) {
isDown = false;
setDotNavigation();
scrollToY(sectionPositions[currentSection], 300, 'easeOutQuint');
if (!iOS()) {
try {
// try to use localStorage
sessionStorage.setItem('saveBodyTop', sectionPositions[currentSection]);
} catch (e) {
// there was an error so...
console.log('local storage is unavailable');
}
}
});
options.container.addEventListener('mousemove', function (e) {
e.stopPropagation();
if (!isDown) return; // stop function if mouse is up
var y = e.pageY;
var walk = (y - startScroll) * 3;
options.container.scrollTop = scrollDown - walk;
});
}
}
function clickNav() {
if (navDots && !iOS()) {
for (var i = 0; i < navDots.length; i++) {
(function (index) {
navDots[i].onclick = function () {
scrollToY(sectionPositions[index], 300, 'easeOutQuint');
};
})(i);
}
}
document.onkeydown = keyPress;
function keyPress(e) {
e = e || window.event;
var newSection;
if (e.keyCode == '38') {
// up arrow
newSection = currentSection < 1 ? 0 : currentSection - 1;
if (sectionPositions[newSection] == sectionPositions[currentSection]) {
newSection = currentSection < 2 ? 0 : currentSection - 2;
}
scrollToY(sectionPositions[newSection], 300, 'easeOutQuint');
}
else if (e.keyCode == '40') {
// down arrow
newSection = currentSection > options.sections.length - 2 ? options.sections.length - 1 : currentSection + 1;
scrollToY(sectionPositions[newSection], 300, 'easeOutQuint');
} else if (e.ctrlKey && e.keyCode === 116) {
sessionStorage.clear('saveBodyTop');
} else if (e.ctrlKey && e.shiftKey && e.keyCode === 82) {
sessionStorage.clear('saveBodyTop');
}
currentSection = newSection;
}
}
window.addEventListener('resize', debounce(onWindowResize, 250, false));
options.container.addEventListener('scroll', function () {
scrollTop = -options.sections[0].getBoundingClientRect().top;
if (!wait) {
getPositionIndex();
setDotNavigation();
if (!iOS()) {
try {
// try to use localStorage
sessionStorage.setItem('saveBodyTop', sectionPositions[currentSection]);
} catch (e) {
// there was an error so...
console.log('local storage is unavailable');
}
}
wait = true;
setTimeout(function () { wait = false; }, 200);
}
}, { passive: true });
function clearScrollOnLink() {
allLinks = document.getElementsByTagName('a') || null;
if (allLinks) {
for (var i = 0, len = allLinks.length; i < len; i++) {
allLinks[i].addEventListener('click', function () {
sessionStorage.clear('saveBodyTop');
}, false);
}
}
}
function removeHiddenSections() {
var i;
for (i = 0; i < options.sections.length; i++) {
if (options.sections[i].offsetWidth > 0 && options.sections[i].offsetHeight > 0) ; else {
options.sections[i].parentNode.removeChild(options.sections[i]);
}
}
}
function iOS() {
var iDevices = [
'iPad Simulator',
'iPhone Simulator',
'iPod Simulator',
'iPad',
'iPhone',
'iPod'
];
if (!!navigator.platform) {
while (iDevices.length) {
if (navigator.platform === iDevices.pop()) { return true; }
}
}
return false;
}
function runOnLoad() {
if (!/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) {
scrollTop = sessionStorage.getItem('saveBodyTop');
}
options.container.scrollTop = scrollTop;
removeHiddenSections();
refreshSectionPositions();
getPositionIndex();
getDots();
setDotNavigation();
clickNav();
mouseGrab();
clearScrollOnLink();
scrollToY(sectionPositions[currentSection], 300, 'easeOutQuint');
}
runOnLoad();
}
/* src/FlipFlop.svelte generated by Svelte v3.24.1 */
function add_css() {
var style = element("style");
style.id = "svelte-qjnk4g-style";
style.textContent = "@media all and (-ms-high-contrast: none), (-ms-high-contrast: active){}@supports (-ms-ime-align: auto){}@media screen and (max-width: 768px){}";
append(document.head, style);
}
function create_fragment(ctx) {
let current;
const default_slot_template = /*$$slots*/ ctx[1].default;
const default_slot = create_slot(default_slot_template, ctx, /*$$scope*/ ctx[0], null);
return {
c() {
if (default_slot) default_slot.c();
},
m(target, anchor) {
if (default_slot) {
default_slot.m(target, anchor);
}
current = true;
},
p(ctx, [dirty]) {
if (default_slot) {
if (default_slot.p && dirty & /*$$scope*/ 1) {
update_slot(default_slot, default_slot_template, ctx, /*$$scope*/ ctx[0], dirty, null, null);
}
}
},
i(local) {
if (current) return;
transition_in(default_slot, local);
current = true;
},
o(local) {
transition_out(default_slot, local);
current = false;
},
d(detaching) {
if (default_slot) default_slot.d(detaching);
}
};
}
function instance($$self, $$props, $$invalidate) {
flipFlop();
let { $$slots = {}, $$scope } = $$props;
$$self.$$set = $$props => {
if ("$$scope" in $$props) $$invalidate(0, $$scope = $$props.$$scope);
};
return [$$scope, $$slots];
}
class FlipFlop extends SvelteComponent {
constructor(options) {
super();
if (!document.getElementById("svelte-qjnk4g-style")) add_css();
init(this, options, instance, create_fragment, safe_not_equal, {});
}
}
return FlipFlop;
})));