]> BookStack Code Mirror - bookstack/blob - resources/js/services/animations.js
Added animation transition for breadcrumb dropdown load
[bookstack] / resources / js / services / animations.js
1 /**
2  * Used in the function below to store references of clean-up functions.
3  * Used to ensure only one transitionend function exists at any time.
4  * @type {WeakMap<object, any>}
5  */
6 const animateStylesCleanupMap = new WeakMap();
7
8 /**
9  * Fade in the given element.
10  * @param {Element} element
11  * @param {Number} animTime
12  * @param {Function|null} onComplete
13  */
14 export function fadeIn(element, animTime = 400, onComplete = null) {
15     cleanupExistingElementAnimation(element);
16     element.style.display = 'block';
17     animateStyles(element, {
18         opacity: ['0', '1']
19     }, animTime, () => {
20         if (onComplete) onComplete();
21     });
22 }
23
24 /**
25  * Fade out the given element.
26  * @param {Element} element
27  * @param {Number} animTime
28  * @param {Function|null} onComplete
29  */
30 export function fadeOut(element, animTime = 400, onComplete = null) {
31     cleanupExistingElementAnimation(element);
32     animateStyles(element, {
33         opacity: ['1', '0']
34     }, animTime, () => {
35         element.style.display = 'none';
36         if (onComplete) onComplete();
37     });
38 }
39
40 /**
41  * Hide the element by sliding the contents upwards.
42  * @param {Element} element
43  * @param {Number} animTime
44  */
45 export function slideUp(element, animTime = 400) {
46     cleanupExistingElementAnimation(element);
47     const currentHeight = element.getBoundingClientRect().height;
48     const computedStyles = getComputedStyle(element);
49     const currentPaddingTop = computedStyles.getPropertyValue('padding-top');
50     const currentPaddingBottom = computedStyles.getPropertyValue('padding-bottom');
51     const animStyles = {
52         height: [`${currentHeight}px`, '0px'],
53         overflow: ['hidden', 'hidden'],
54         paddingTop: [currentPaddingTop, '0px'],
55         paddingBottom: [currentPaddingBottom, '0px'],
56     };
57
58     animateStyles(element, animStyles, animTime, () => {
59         element.style.display = 'none';
60     });
61 }
62
63 /**
64  * Show the given element by expanding the contents.
65  * @param {Element} element - Element to animate
66  * @param {Number} animTime - Animation time in ms
67  */
68 export function slideDown(element, animTime = 400) {
69     cleanupExistingElementAnimation(element);
70     element.style.display = 'block';
71     const targetHeight = element.getBoundingClientRect().height;
72     const computedStyles = getComputedStyle(element);
73     const targetPaddingTop = computedStyles.getPropertyValue('padding-top');
74     const targetPaddingBottom = computedStyles.getPropertyValue('padding-bottom');
75     const animStyles = {
76         height: ['0px', `${targetHeight}px`],
77         overflow: ['hidden', 'hidden'],
78         paddingTop: ['0px', targetPaddingTop],
79         paddingBottom: ['0px', targetPaddingBottom],
80     };
81
82     animateStyles(element, animStyles, animTime);
83 }
84
85 /**
86  * Transition the height of the given element between two states.
87  * Call with first state, and you'll receive a function in return.
88  * Call the returned function in the second state to animate between those two states.
89  * If animating to/from 0-height use the slide-up/slide down as easier alternatives.
90  * @param {Element} element - Element to animate
91  * @param {Number} animTime - Animation time in ms
92  * @returns {function} - Function to run in second state to trigger animation.
93  */
94 export function transitionHeight(element, animTime = 400) {
95     const startHeight = element.getBoundingClientRect().height;
96     const initialComputedStyles = getComputedStyle(element);
97     const startPaddingTop = initialComputedStyles.getPropertyValue('padding-top');
98     const startPaddingBottom = initialComputedStyles.getPropertyValue('padding-bottom');
99
100     return () => {
101         cleanupExistingElementAnimation(element);
102         const targetHeight = element.getBoundingClientRect().height;
103         const computedStyles = getComputedStyle(element);
104         const targetPaddingTop = computedStyles.getPropertyValue('padding-top');
105         const targetPaddingBottom = computedStyles.getPropertyValue('padding-bottom');
106         const animStyles = {
107             height: [`${startHeight}px`, `${targetHeight}px`],
108             overflow: ['hidden', 'hidden'],
109             paddingTop: [startPaddingTop, targetPaddingTop],
110             paddingBottom: [startPaddingBottom, targetPaddingBottom],
111         };
112
113         animateStyles(element, animStyles, animTime);
114     };
115 }
116
117 /**
118  * Animate the css styles of an element using FLIP animation techniques.
119  * Styles must be an object where the keys are style properties, camelcase, and the values
120  * are an array of two items in the format [initialValue, finalValue]
121  * @param {Element} element
122  * @param {Object} styles
123  * @param {Number} animTime
124  * @param {Function} onComplete
125  */
126 function animateStyles(element, styles, animTime = 400, onComplete = null) {
127     const styleNames = Object.keys(styles);
128     for (let style of styleNames) {
129         element.style[style] = styles[style][0];
130     }
131
132     const cleanup = () => {
133         for (let style of styleNames) {
134             element.style[style] = null;
135         }
136         element.style.transition = null;
137         element.removeEventListener('transitionend', cleanup);
138         animateStylesCleanupMap.delete(element);
139         if (onComplete) onComplete();
140     };
141
142     setTimeout(() => {
143         element.style.transition = `all ease-in-out ${animTime}ms`;
144         for (let style of styleNames) {
145             element.style[style] = styles[style][1];
146         }
147
148         element.addEventListener('transitionend', cleanup);
149         animateStylesCleanupMap.set(element, cleanup);
150     }, 15);
151 }
152
153 /**
154  * Run the active cleanup action for the given element.
155  * @param {Element} element
156  */
157 function cleanupExistingElementAnimation(element) {
158     if (animateStylesCleanupMap.has(element)) {
159         const oldCleanup = animateStylesCleanupMap.get(element);
160         oldCleanup();
161     }
162 }