UNPKG

tw-slide-toggle

Version:

Quick & dirty replacement for jQuery's .slideToggle functionality

209 lines (175 loc) 5.9 kB
const defaultConfig = { duration: 350, display: 'block', ease: 'easeInOut', callback: () => {} }; let instances = []; let animating = false; const eases = { linear: progress => progress, easeIn: progress => Math.pow(progress, 2), easeOut: progress => progress * (2 - progress), easeInOut: progress => progress < .5 ? 2 * Math.pow(progress, 2) : -1 + (4 - 2 * progress) * progress }; const styleProperties = { height: 'height', paddingTop: 'padding-top', paddingBottom: 'padding-bottom', marginTop: 'margin-top', marginBottom: 'margin-bottom', borderTopWidth: 'border-top-width', borderBottomWidth: 'border-bottom-width' }; const getInstance = (element) => { return instances.find(instance => instance.element === element); }; const createInstance = (element, config = {}) => { const isSlidingDown = 'down' == config.direction; const startSize = isSlidingDown ? getHiddenSize(element) : getCurrentSize(element); const endSize = isSlidingDown ? getTotalSize(element) : getHiddenSize(element); const progress = 0; const instance = { element, startSize, endSize, progress, ...defaultConfig, ...config }; instances.push(instance); return instance; }; const getOrCreateInstance = (element, config = {}) => { let instance = getInstance(element); if (!instance) instance = createInstance(element, config); else updateInstance(instance, config); return instance; }; const updateInstance = (instance, config = {}) => { const { element, progress, direction } = instance; if (config.direction && config.direction != direction) { const isSlidingDown = 'down' == config.direction; config.startSize = getCurrentSize(element); config.endSize = isSlidingDown ? getTotalSize(element) : getHiddenSize(element); config.startTime = 0; config.endTime = 0; if (config.duration) config.duration *= progress; else config.duration = instance.duration * progress; } Object.assign(instance, config); return instance; }; const removeInstance = (element) => { instances = instances.filter(instance => instance.element !== element); return instances; }; const animateInstance = (instance, timestamp) => { const { element, duration } = instance; if (!instance.startTime) instance.startTime = timestamp; if (!instance.endTime) instance.endTime = instance.startTime + duration; instance.currentTime = timestamp; const { display, startSize, endSize, direction, ease, callback, startTime, endTime, currentTime } = instance; const isSlidingDown = 'down' == direction; const progress = Math.min((currentTime - startTime) / duration, 1); const easeProgress = eases[ease](progress); instance.progress = progress; setSize(element, startSize, endSize, easeProgress); element.style.overflow = 'hidden'; element.style.display = display; if (easeProgress >= 1) { if (!isSlidingDown) element.style.display = 'none'; element.style.overflow = ''; callback(isSlidingDown, element); removeInstance(element); } }; const number = (string) => { return parseFloat(string) || 0; }; const getStyles = (element) => { return Object.keys(styleProperties).reduce((accumulator, current) => { accumulator[current] = element.style[current]; return accumulator; }, {}); }; const getCurrentSize = (element) => { const style = window.getComputedStyle(element); return Object.keys(styleProperties).reduce((accumulator, current) => { accumulator[current] = number(style.getPropertyValue(styleProperties[current])); return accumulator; }, {}); }; const getTotalSize = (element) => { const instance = getInstance(element); const currentStyles = getStyles(element); const currentDisplay = element.style.display; Object.keys(currentStyles).forEach(key => element.style[key] = ''); element.style.display = instance ? instance.display : defaultConfig.display; const totalSize = getCurrentSize(element); Object.keys(currentStyles).forEach(key => element.style[key] = currentStyles[key]); element.style.display = currentDisplay; return totalSize; }; const getHiddenSize = (element) => { return Object.keys(styleProperties).reduce((accumulator, current) => { accumulator[current] = 0; return accumulator; }, {}); }; const setSize = (element, startSize, endSize, progress) => { Object.keys(startSize).forEach(key => { if (progress >= 1) element.style[key] = ''; else { const value = startSize[key] + (progress * (endSize[key] - startSize[key])); element.style[key] = `${value}px`; } }); }; const isHidden = (element) => { const instance = getInstance(element); return (instance && instance.direction == 'up') || !element.offsetHeight; }; const animate = (timestamp) => { instances.forEach(instance => animateInstance(instance, timestamp)); if (instances.length) requestAnimationFrame(animate); else stop(); }; const start = () => { if (!animating) { animating = true; requestAnimationFrame(animate); } }; const stop = () => { animating = false; }; const slideDown = (element, config = {}) => { const instanceConfig = { direction: 'down', ...config }; const instance = getOrCreateInstance(element, instanceConfig); start(); }; const slideUp = (element, config = {}) => { const instanceConfig = { direction: 'up', ...config }; const instance = getOrCreateInstance(element, instanceConfig); start(); }; const slideToggle = (element, config = {}) => { if (isHidden(element)) slideDown(element, config); else slideUp(element, config); }; export { slideDown, slideUp, slideToggle };