tw-slide-toggle
Version:
Quick & dirty replacement for jQuery's .slideToggle functionality
209 lines (175 loc) • 5.9 kB
JavaScript
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
};