diff --git a/resources/assets/js/components/collapsible.js b/resources/assets/js/components/collapsible.js index a13b367d3..40ab32508 100644 --- a/resources/assets/js/components/collapsible.js +++ b/resources/assets/js/components/collapsible.js @@ -1,3 +1,5 @@ +import {slideDown, slideUp} from "../services/animations"; + /** * Collapsible * Provides some simple logic to allow collapsible sections. @@ -16,12 +18,12 @@ class Collapsible { open() { this.elem.classList.add('open'); - $(this.content).slideDown(400); + slideDown(this.content, 300); } close() { this.elem.classList.remove('open'); - $(this.content).slideUp(400); + slideUp(this.content, 300); } toggle() { diff --git a/resources/assets/js/services/animations.js b/resources/assets/js/services/animations.js new file mode 100644 index 000000000..5cb90b70c --- /dev/null +++ b/resources/assets/js/services/animations.js @@ -0,0 +1,91 @@ +/** + * Hide the element by sliding the contents upwards. + * @param {Element} element + * @param {Number} animTime + */ +export function slideUp(element, animTime = 400) { + const currentHeight = element.getBoundingClientRect().height; + const computedStyles = getComputedStyle(element); + const currentPaddingTop = computedStyles.getPropertyValue('padding-top'); + const currentPaddingBottom = computedStyles.getPropertyValue('padding-bottom'); + const animStyles = { + height: [`${currentHeight}px`, '0px'], + overflow: ['hidden', 'hidden'], + paddingTop: [currentPaddingTop, '0px'], + paddingBottom: [currentPaddingBottom, '0px'], + }; + + animateStyles(element, animStyles, animTime, () => { + element.style.display = 'none'; + }); +} + +/** + * Show the given element by expanding the contents. + * @param {Element} element - Element to animate + * @param {Number} animTime - Animation time in ms + */ +export function slideDown(element, animTime = 400) { + element.style.display = 'block'; + const targetHeight = element.getBoundingClientRect().height; + const computedStyles = getComputedStyle(element); + const targetPaddingTop = computedStyles.getPropertyValue('padding-top'); + const targetPaddingBottom = computedStyles.getPropertyValue('padding-bottom'); + const animStyles = { + height: ['0px', `${targetHeight}px`], + overflow: ['hidden', 'hidden'], + paddingTop: ['0px', targetPaddingTop], + paddingBottom: ['0px', targetPaddingBottom], + }; + + animateStyles(element, animStyles, animTime); +} + +/** + * Used in the function below to store references of clean-up functions. + * Used to ensure only one transitionend function exists at any time. + * @type {WeakMap} + */ +const animateStylesCleanupMap = new WeakMap(); + +/** + * Animate the css styles of an element using FLIP animation techniques. + * Styles must be an object where the keys are style properties, camelcase, and the values + * are an array of two items in the format [initialValue, finalValue] + * @param {Element} element + * @param {Object} styles + * @param {Number} animTime + * @param {Function} onComplete + */ +function animateStyles(element, styles, animTime = 400, onComplete = null) { + const styleNames = Object.keys(styles); + for (let style of styleNames) { + element.style[style] = styles[style][0]; + } + + const cleanup = () => { + for (let style of styleNames) { + element.style[style] = null; + } + element.style.transition = null; + element.removeEventListener('transitionend', cleanup); + if (onComplete) onComplete(); + }; + + setTimeout(() => { + requestAnimationFrame(() => { + element.style.transition = `all ease-in-out ${animTime}ms`; + for (let style of styleNames) { + element.style[style] = styles[style][1]; + } + + if (animateStylesCleanupMap.has(element)) { + const oldCleanup = animateStylesCleanupMap.get(element); + element.removeEventListener('transitionend', oldCleanup); + } + + element.addEventListener('transitionend', cleanup); + animateStylesCleanupMap.set(element, cleanup); + }); + }, 10); +} \ No newline at end of file