UNPKG

flipflop-scroll

Version:

A tiny vanilla JS library to scroll section by section

838 lines (727 loc) 25.9 kB
(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; })));