]> BookStack Code Mirror - bookstack/blob - resources/js/services/animations.ts
Deps: Updated composer/npm packages, fixed test namespace
[bookstack] / resources / js / services / animations.ts
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  */
5 const animateStylesCleanupMap: WeakMap<object, any> = new WeakMap();
6
7 /**
8  * Animate the css styles of an element using FLIP animation techniques.
9  * Styles must be an object where the keys are style rule names and the values
10  * are an array of two items in the format [initialValue, finalValue]
11  */
12 function animateStyles(
13     element: HTMLElement,
14     styles: Record<string, string[]>,
15     animTime: number = 400,
16     onComplete: Function | null = null
17 ): void {
18     const styleNames = Object.keys(styles);
19     for (const style of styleNames) {
20         element.style.setProperty(style, styles[style][0]);
21     }
22
23     const cleanup = () => {
24         for (const style of styleNames) {
25             element.style.removeProperty(style);
26         }
27         element.style.removeProperty('transition');
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.setProperty(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  */
47 function cleanupExistingElementAnimation(element: Element) {
48     if (animateStylesCleanupMap.has(element)) {
49         const oldCleanup = animateStylesCleanupMap.get(element);
50         oldCleanup();
51     }
52 }
53
54 /**
55  * Fade in the given element.
56  */
57 export function fadeIn(element: HTMLElement, animTime: number = 400, onComplete: Function | null = null): void {
58     cleanupExistingElementAnimation(element);
59     element.style.display = 'block';
60     animateStyles(element, {
61         'opacity': ['0', '1'],
62     }, animTime, () => {
63         if (onComplete) onComplete();
64     });
65 }
66
67 /**
68  * Fade out the given element.
69  */
70 export function fadeOut(element: HTMLElement, animTime: number = 400, onComplete: Function | null = null): void {
71     cleanupExistingElementAnimation(element);
72     animateStyles(element, {
73         'opacity': ['1', '0'],
74     }, animTime, () => {
75         element.style.display = 'none';
76         if (onComplete) onComplete();
77     });
78 }
79
80 /**
81  * Hide the element by sliding the contents upwards.
82  */
83 export function slideUp(element: HTMLElement, animTime: number = 400) {
84     cleanupExistingElementAnimation(element);
85     const currentHeight = element.getBoundingClientRect().height;
86     const computedStyles = getComputedStyle(element);
87     const currentPaddingTop = computedStyles.getPropertyValue('padding-top');
88     const currentPaddingBottom = computedStyles.getPropertyValue('padding-bottom');
89     const animStyles = {
90         'max-height': [`${currentHeight}px`, '0px'],
91         'overflow': ['hidden', 'hidden'],
92         'padding-top': [currentPaddingTop, '0px'],
93         'padding-bottom': [currentPaddingBottom, '0px'],
94     };
95
96     animateStyles(element, animStyles, animTime, () => {
97         element.style.display = 'none';
98     });
99 }
100
101 /**
102  * Show the given element by expanding the contents.
103  */
104 export function slideDown(element: HTMLElement, animTime: number = 400) {
105     cleanupExistingElementAnimation(element);
106     element.style.display = 'block';
107     const targetHeight = element.getBoundingClientRect().height;
108     const computedStyles = getComputedStyle(element);
109     const targetPaddingTop = computedStyles.getPropertyValue('padding-top');
110     const targetPaddingBottom = computedStyles.getPropertyValue('padding-bottom');
111     const animStyles = {
112         'max-height': ['0px', `${targetHeight}px`],
113         'overflow': ['hidden', 'hidden'],
114         'padding-top': ['0px', targetPaddingTop],
115         'padding-bottom': ['0px', targetPaddingBottom],
116     };
117
118     animateStyles(element, animStyles, animTime);
119 }
120
121 /**
122  * Transition the height of the given element between two states.
123  * Call with first state, and you'll receive a function in return.
124  * Call the returned function in the second state to animate between those two states.
125  * If animating to/from 0-height use the slide-up/slide down as easier alternatives.
126  */
127 export function transitionHeight(element: HTMLElement, animTime: number = 400): () => void {
128     const startHeight = element.getBoundingClientRect().height;
129     const initialComputedStyles = getComputedStyle(element);
130     const startPaddingTop = initialComputedStyles.getPropertyValue('padding-top');
131     const startPaddingBottom = initialComputedStyles.getPropertyValue('padding-bottom');
132
133     return () => {
134         cleanupExistingElementAnimation(element);
135         const targetHeight = element.getBoundingClientRect().height;
136         const computedStyles = getComputedStyle(element);
137         const targetPaddingTop = computedStyles.getPropertyValue('padding-top');
138         const targetPaddingBottom = computedStyles.getPropertyValue('padding-bottom');
139         const animStyles = {
140             'height': [`${startHeight}px`, `${targetHeight}px`],
141             'overflow': ['hidden', 'hidden'],
142             'padding-top': [startPaddingTop, targetPaddingTop],
143             'padding-bottom': [startPaddingBottom, targetPaddingBottom],
144         };
145
146         animateStyles(element, animStyles, animTime);
147     };
148 }
Morty Proxy This is a proxified and sanitized view of the page, visit original site.