1 | /*
|
2 | * Copyright 2020 Adobe. All rights reserved.
|
3 | * This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
4 | * you may not use this file except in compliance with the License. You may obtain a copy
|
5 | * of the License at http://www.apache.org/licenses/LICENSE-2.0
|
6 | *
|
7 | * Unless required by applicable law or agreed to in writing, software distributed under
|
8 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
9 | * OF ANY KIND, either express or implied. See the License for the specific language
|
10 | * governing permissions and limitations under the License.
|
11 | */
|
12 |
|
13 | // We store a global list of elements that are currently transitioning,
|
14 | // mapped to a set of CSS properties that are transitioning for that element.
|
15 | // This is necessary rather than a simple count of transitions because of browser
|
16 | // bugs, e.g. Chrome sometimes fires both transitionend and transitioncancel rather
|
17 | // than one or the other. So we need to track what's actually transitioning so that
|
18 | // we can ignore these duplicate events.
|
19 | let transitionsByElement = new Map<EventTarget, Set<string>>();
|
20 |
|
21 | // A list of callbacks to call once there are no transitioning elements.
|
22 | let transitionCallbacks = new Set<() => void>();
|
23 |
|
24 | function setupGlobalEvents() {
|
25 | if (typeof window === 'undefined') {
|
26 | return;
|
27 | }
|
28 |
|
29 | let onTransitionStart = (e: TransitionEvent) => {
|
30 | // Add the transitioning property to the list for this element.
|
31 | let transitions = transitionsByElement.get(e.target);
|
32 | if (!transitions) {
|
33 | transitions = new Set();
|
34 | transitionsByElement.set(e.target, transitions);
|
35 |
|
36 | // The transitioncancel event must be registered on the element itself, rather than as a global
|
37 | // event. This enables us to handle when the node is deleted from the document while it is transitioning.
|
38 | // In that case, the cancel event would have nowhere to bubble to so we need to handle it directly.
|
39 | e.target.addEventListener('transitioncancel', onTransitionEnd);
|
40 | }
|
41 |
|
42 | transitions.add(e.propertyName);
|
43 | };
|
44 |
|
45 | let onTransitionEnd = (e: TransitionEvent) => {
|
46 | // Remove property from list of transitioning properties.
|
47 | let properties = transitionsByElement.get(e.target);
|
48 | if (!properties) {
|
49 | return;
|
50 | }
|
51 |
|
52 | properties.delete(e.propertyName);
|
53 |
|
54 | // If empty, remove transitioncancel event, and remove the element from the list of transitioning elements.
|
55 | if (properties.size === 0) {
|
56 | e.target.removeEventListener('transitioncancel', onTransitionEnd);
|
57 | transitionsByElement.delete(e.target);
|
58 | }
|
59 |
|
60 | // If no transitioning elements, call all of the queued callbacks.
|
61 | if (transitionsByElement.size === 0) {
|
62 | for (let cb of transitionCallbacks) {
|
63 | cb();
|
64 | }
|
65 |
|
66 | transitionCallbacks.clear();
|
67 | }
|
68 | };
|
69 |
|
70 | document.body.addEventListener('transitionrun', onTransitionStart);
|
71 | document.body.addEventListener('transitionend', onTransitionEnd);
|
72 | }
|
73 |
|
74 | if (typeof document !== 'undefined') {
|
75 | if (document.readyState !== 'loading') {
|
76 | setupGlobalEvents();
|
77 | } else {
|
78 | document.addEventListener('DOMContentLoaded', setupGlobalEvents);
|
79 | }
|
80 | }
|
81 |
|
82 | export function runAfterTransition(fn: () => void) {
|
83 | // Wait one frame to see if an animation starts, e.g. a transition on mount.
|
84 | requestAnimationFrame(() => {
|
85 | // If no transitions are running, call the function immediately.
|
86 | // Otherwise, add it to a list of callbacks to run at the end of the animation.
|
87 | if (transitionsByElement.size === 0) {
|
88 | fn();
|
89 | } else {
|
90 | transitionCallbacks.add(fn);
|
91 | }
|
92 | });
|
93 | }
|