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