All Downloads are FREE. Search and download functionalities are using the official Maven repository.

package.src.animations.animate-css.js Maven / Gradle / Ivy

import { isDefined } from "../shared/utils";
import {
  TRANSITION_DURATION_PROP,
  TRANSITION_DELAY_PROP,
  ANIMATION_DELAY_PROP,
  TRANSITION_PROP,
  PROPERTY_KEY,
  ANIMATION_DURATION_PROP,
  ANIMATION_ITERATION_COUNT_KEY,
  ANIMATION_PROP,
  DURATION_KEY,
  applyAnimationClassesFactory,
  pendClasses,
  prepareAnimationOptions,
  getDomNode,
  packageStyles,
  EVENT_CLASS_PREFIX,
  ADD_CLASS_SUFFIX,
  REMOVE_CLASS_SUFFIX,
  applyInlineStyle,
  SAFE_FAST_FORWARD_DURATION_VALUE,
  ACTIVE_CLASS_SUFFIX,
  applyAnimationFromStyles,
  applyAnimationStyles,
  blockKeyframeAnimations,
  removeFromArray,
  TIMING_KEY,
  TRANSITIONEND_EVENT,
  ANIMATIONEND_EVENT,
  applyAnimationToStyles,
} from "./shared";

const ANIMATE_TIMER_KEY = "$$animateCss";

const ONE_SECOND = 1000;

const ELAPSED_TIME_MAX_DECIMAL_PLACES = 3;
const CLOSING_TIME_BUFFER = 1.5;

const DETECT_CSS_PROPERTIES = {
  transitionDuration: TRANSITION_DURATION_PROP,
  transitionDelay: TRANSITION_DELAY_PROP,
  transitionProperty: TRANSITION_PROP + PROPERTY_KEY,
  animationDuration: ANIMATION_DURATION_PROP,
  animationDelay: ANIMATION_DELAY_PROP,
  animationIterationCount: ANIMATION_PROP + ANIMATION_ITERATION_COUNT_KEY,
};

const DETECT_STAGGER_CSS_PROPERTIES = {
  transitionDuration: TRANSITION_DURATION_PROP,
  transitionDelay: TRANSITION_DELAY_PROP,
  animationDuration: ANIMATION_DURATION_PROP,
  animationDelay: ANIMATION_DELAY_PROP,
};

function getCssKeyframeDurationStyle(duration) {
  return [ANIMATION_DURATION_PROP, `${duration}s`];
}

function getCssDelayStyle(delay, isKeyframeAnimation) {
  const prop = isKeyframeAnimation
    ? ANIMATION_DELAY_PROP
    : TRANSITION_DELAY_PROP;
  return [prop, `${delay}s`];
}

function computeCssStyles(element, properties) {
  const styles = Object.create(null);
  const detectedStyles = window.getComputedStyle(element) || {};
  Object.entries(properties).forEach(([actualStyleName, formalStyleName]) => {
    let val = detectedStyles[formalStyleName];
    if (val) {
      const c = val.charAt(0);

      // only numerical-based values have a negative sign or digit as the first value
      if (c === "-" || c === "+" || c >= 0) {
        val = parseMaxTime(val);
      }

      // by setting this to null in the event that the delay is not set or is set directly as 0
      // then we can still allow for negative values to be used later on and not mistake this
      // value for being greater than any other negative value.
      if (val === 0) {
        val = null;
      }
      styles[actualStyleName] = val;
    }
  });

  return styles;
}

function parseMaxTime(str) {
  let maxValue = 0;
  str.split(/\s*,\s*/).forEach((value) => {
    // it's always safe to consider only second values and omit `ms` values since
    // getComputedStyle will always handle the conversion for us
    if (value.charAt(value.length - 1) === "s") {
      value = value.substring(0, value.length - 1);
    }
    value = parseFloat(value) || 0;
    maxValue = maxValue ? Math.max(value, maxValue) : value;
  });
  return maxValue;
}

function truthyTimingValue(val) {
  return val === 0 || val != null;
}

function getCssTransitionDurationStyle(duration, applyOnlyDuration) {
  let style = TRANSITION_PROP;
  let value = `${duration}s`;
  if (applyOnlyDuration) {
    style += DURATION_KEY;
  } else {
    value += " linear all";
  }
  return [style, value];
}

// we do not reassign an already present style value since
// if we detect the style property value again we may be
// detecting styles that were added via the `from` styles.
// We make use of `isDefined` here since an empty string
// or null value (which is what getPropertyValue will return
// for a non-existing style) will still be marked as a valid
// value for the style (a falsy value implies that the style
// is to be removed at the end of the animation). If we had a simple
// "OR" statement then it would not be enough to catch that.
function registerRestorableStyles(backup, node, properties) {
  properties.forEach((prop) => {
    backup[prop] = isDefined(backup[prop])
      ? backup[prop]
      : node.style.getPropertyValue(prop);
  });
}

export function AnimateCssProvider() {
  this.$get = [
    "$$AnimateRunner",
    "$$animateCache",
    "$$rAFScheduler",
    "$$animateQueue",

    /**
     *
     * @param {*} $$AnimateRunner
     * @param {*} $$animateCache
     * @param {import("./raf-scheduler").RafScheduler} $$rAFScheduler
     * @param {*} $$animateQueue
     * @returns
     */
    function ($$AnimateRunner, $$animateCache, $$rAFScheduler, $$animateQueue) {
      const applyAnimationClasses = applyAnimationClassesFactory();

      function computeCachedCssStyles(
        node,
        cacheKey,
        allowNoDuration,
        properties,
      ) {
        let timings = $$animateCache.get(cacheKey);

        if (!timings) {
          timings = computeCssStyles(node, properties);
          if (timings.animationIterationCount === "infinite") {
            timings.animationIterationCount = 1;
          }
        }

        // if a css animation has no duration we
        // should mark that so that repeated addClass/removeClass calls are skipped
        const hasDuration =
          allowNoDuration ||
          timings.transitionDuration > 0 ||
          timings.animationDuration > 0;

        // we keep putting this in multiple times even though the value and the cacheKey are the same
        // because we're keeping an internal tally of how many duplicate animations are detected.
        $$animateCache.put(cacheKey, timings, hasDuration);

        return timings;
      }

      function computeCachedCssStaggerStyles(
        node,
        className,
        cacheKey,
        properties,
      ) {
        let stagger;
        const staggerCacheKey = `stagger-${cacheKey}`;

        // if we have one or more existing matches of matching elements
        // containing the same parent + CSS styles (which is how cacheKey works)
        // then staggering is possible
        if ($$animateCache.count(cacheKey) > 0) {
          stagger = $$animateCache.get(staggerCacheKey);

          if (!stagger) {
            const staggerClassName = pendClasses(className, "-stagger");

            node.className += ` ${staggerClassName}`;
            stagger = computeCssStyles(node, properties);

            // force the conversion of a null value to zero incase not set
            stagger.animationDuration = Math.max(stagger.animationDuration, 0);
            stagger.transitionDuration = Math.max(
              stagger.transitionDuration,
              0,
            );

            node.classList.remove(staggerClassName);

            $$animateCache.put(staggerCacheKey, stagger, true);
          }
        }

        return stagger || {};
      }

      const rafWaitQueue = [];
      function waitUntilQuiet(callback) {
        rafWaitQueue.push(callback);
        $$rAFScheduler.waitUntilQuiet(() => {
          $$animateCache.flush();

          // DO NOT REMOVE THIS LINE OR REFACTOR OUT THE `pageWidth` variable.
          // the line below will force the browser to perform a repaint so
          // that all the animated elements within the animation frame will
          // be properly updated and drawn on screen. This is required to
          // ensure that the preparation animation is properly flushed so that
          // the active state picks up from there. DO NOT REMOVE THIS LINE.
          // DO NOT OPTIMIZE THIS LINE. THE MINIFIER WILL REMOVE IT OTHERWISE WHICH
          // WILL RESULT IN AN UNPREDICTABLE BUG THAT IS VERY HARD TO TRACK DOWN AND
          // WILL TAKE YEARS AWAY FROM YOUR LIFE.

          const pageWidth = document.body.offsetWidth + 1;

          // we use a for loop to ensure that if the queue is changed
          // during this looping then it will consider new requests
          for (let i = 0; i < rafWaitQueue.length; i++) {
            rafWaitQueue[i](pageWidth);
          }
          rafWaitQueue.length = 0;
        });
      }

      function computeTimings(node, cacheKey, allowNoDuration) {
        const timings = computeCachedCssStyles(
          node,
          cacheKey,
          allowNoDuration,
          DETECT_CSS_PROPERTIES,
        );
        const aD = timings.animationDelay;
        const tD = timings.transitionDelay;
        timings.maxDelay = aD && tD ? Math.max(aD, tD) : aD || tD;
        timings.maxDuration = Math.max(
          timings.animationDuration * timings.animationIterationCount,
          timings.transitionDuration,
        );

        return timings;
      }

      return function init(element, initialOptions) {
        // all of the animation functions should create
        // a copy of the options data, however, if a
        // parent service has already created a copy then
        // we should stick to using that
        let options = initialOptions || {};
        if (!options.$$prepared) {
          options = prepareAnimationOptions(structuredClone(options));
        }

        const restoreStyles = {};
        const node = /** @type {HTMLElement} */ (getDomNode(element));
        if (!node || !node.parentNode || !$$animateQueue.enabled()) {
          return closeAndReturnNoopAnimator();
        }

        const temporaryStyles = [];
        const styles = packageStyles(options);
        let animationClosed;
        let animationPaused;
        let animationCompleted;
        let runner;
        let runnerHost;
        let maxDelay;
        let maxDelayTime;
        let maxDuration;
        let maxDurationTime;
        let startTime;
        const events = [];

        if (options.duration === 0) {
          return closeAndReturnNoopAnimator();
        }

        const method =
          options.event && Array.isArray(options.event)
            ? options.event.join(" ")
            : options.event;

        const isStructural = method && options.structural;
        let structuralClassName = "";
        let addRemoveClassName = "";

        if (isStructural) {
          structuralClassName = pendClasses(method, EVENT_CLASS_PREFIX, true);
        } else if (method) {
          structuralClassName = method;
        }

        if (options.addClass) {
          addRemoveClassName += pendClasses(options.addClass, ADD_CLASS_SUFFIX);
        }

        if (options.removeClass) {
          if (addRemoveClassName.length) {
            addRemoveClassName += " ";
          }
          addRemoveClassName += pendClasses(
            options.removeClass,
            REMOVE_CLASS_SUFFIX,
          );
        }

        // there may be a situation where a structural animation is combined together
        // with CSS classes that need to resolve before the animation is computed.
        // However this means that there is no explicit CSS code to block the animation
        // from happening (by setting 0s none in the class name). If this is the case
        // we need to apply the classes before the first rAF so we know to continue if
        // there actually is a detected transition or keyframe animation
        if (options.applyClassesEarly && addRemoveClassName.length) {
          applyAnimationClasses(element, options);
        }

        let preparationClasses = [structuralClassName, addRemoveClassName]
          .join(" ")
          .trim();
        const hasToStyles = styles.to && Object.keys(styles.to).length > 0;
        const containsKeyframeAnimation =
          (options.keyframeStyle || "").length > 0;

        // there is no way we can trigger an animation if no styles and
        // no classes are being applied which would then trigger a transition,
        // unless there a is raw keyframe value that is applied to the element.
        if (!containsKeyframeAnimation && !hasToStyles && !preparationClasses) {
          return closeAndReturnNoopAnimator();
        }

        let stagger;
        let cacheKey = $$animateCache.cacheKey(
          node,
          method,
          options.addClass,
          options.removeClass,
        );
        if ($$animateCache.containsCachedAnimationWithoutDuration(cacheKey)) {
          preparationClasses = null;
          return closeAndReturnNoopAnimator();
        }

        if (options.stagger > 0) {
          const staggerVal = parseFloat(options.stagger);
          stagger = {
            transitionDelay: staggerVal,
            animationDelay: staggerVal,
            transitionDuration: 0,
            animationDuration: 0,
          };
        } else {
          stagger = computeCachedCssStaggerStyles(
            node,
            preparationClasses,
            cacheKey,
            DETECT_STAGGER_CSS_PROPERTIES,
          );
        }

        if (!options.$$skipPreparationClasses) {
          element[0].classList.add(
            ...preparationClasses.split(" ").filter((x) => x !== ""),
          );
        }

        let applyOnlyDuration;

        if (options.transitionStyle) {
          const transitionStyle = [TRANSITION_PROP, options.transitionStyle];
          applyInlineStyle(node, transitionStyle);
          temporaryStyles.push(transitionStyle);
        }

        if (options.duration >= 0) {
          applyOnlyDuration = node.style[TRANSITION_PROP].length > 0;
          const durationStyle = getCssTransitionDurationStyle(
            options.duration,
            applyOnlyDuration,
          );

          // we set the duration so that it will be picked up by getComputedStyle later
          applyInlineStyle(node, durationStyle);

          temporaryStyles.push(durationStyle);
        }

        if (options.keyframeStyle) {
          const keyframeStyle = [ANIMATION_PROP, options.keyframeStyle];
          applyInlineStyle(node, keyframeStyle);
          temporaryStyles.push(keyframeStyle);
        }

        const itemIndex = stagger
          ? options.staggerIndex >= 0
            ? options.staggerIndex
            : $$animateCache.count(cacheKey)
          : 0;

        const isFirst = itemIndex === 0;

        // this is a pre-emptive way of forcing the setup classes to be added and applied INSTANTLY
        // without causing any combination of transitions to kick in. By adding a negative delay value
        // it forces the setup class' transition to end immediately. We later then remove the negative
        // transition delay to allow for the transition to naturally do it's thing. The beauty here is
        // that if there is no transition defined then nothing will happen and this will also allow
        // other transitions to be stacked on top of each other without any chopping them out.
        if (isFirst && !options.skipBlocking) {
          blockTransitions(node, SAFE_FAST_FORWARD_DURATION_VALUE);
        }

        let timings = computeTimings(node, cacheKey, !isStructural);
        let relativeDelay = timings.maxDelay;
        maxDelay = Math.max(relativeDelay, 0);
        maxDuration = timings.maxDuration;

        const flags = {};
        flags.hasTransitions = timings.transitionDuration > 0;
        flags.hasAnimations = timings.animationDuration > 0;
        flags.hasTransitionAll =
          flags.hasTransitions && timings.transitionProperty === "all";
        flags.applyTransitionDuration =
          hasToStyles &&
          ((flags.hasTransitions && !flags.hasTransitionAll) ||
            (flags.hasAnimations && !flags.hasTransitions));
        flags.applyAnimationDuration = options.duration && flags.hasAnimations;
        flags.applyTransitionDelay =
          truthyTimingValue(options.delay) &&
          (flags.applyTransitionDuration || flags.hasTransitions);
        flags.applyAnimationDelay =
          truthyTimingValue(options.delay) && flags.hasAnimations;
        flags.recalculateTimingStyles = addRemoveClassName.length > 0;

        if (flags.applyTransitionDuration || flags.applyAnimationDuration) {
          maxDuration = options.duration
            ? parseFloat(options.duration)
            : maxDuration;

          if (flags.applyTransitionDuration) {
            flags.hasTransitions = true;
            timings.transitionDuration = maxDuration;
            applyOnlyDuration =
              node.style[TRANSITION_PROP + PROPERTY_KEY].length > 0;
            temporaryStyles.push(
              getCssTransitionDurationStyle(maxDuration, applyOnlyDuration),
            );
          }

          if (flags.applyAnimationDuration) {
            flags.hasAnimations = true;
            timings.animationDuration = maxDuration;
            temporaryStyles.push(getCssKeyframeDurationStyle(maxDuration));
          }
        }

        if (maxDuration === 0 && !flags.recalculateTimingStyles) {
          return closeAndReturnNoopAnimator();
        }

        var activeClasses = pendClasses(
          preparationClasses,
          ACTIVE_CLASS_SUFFIX,
        );

        if (options.delay != null) {
          var delayStyle;
          if (typeof options.delay !== "boolean") {
            delayStyle = parseFloat(options.delay);
            // number in options.delay means we have to recalculate the delay for the closing timeout
            maxDelay = Math.max(delayStyle, 0);
          }

          if (flags.applyTransitionDelay) {
            temporaryStyles.push(getCssDelayStyle(delayStyle));
          }

          if (flags.applyAnimationDelay) {
            temporaryStyles.push(getCssDelayStyle(delayStyle, true));
          }
        }

        // we need to recalculate the delay value since we used a pre-emptive negative
        // delay value and the delay value is required for the final event checking. This
        // property will ensure that this will happen after the RAF phase has passed.
        if (options.duration == null && timings.transitionDuration > 0) {
          flags.recalculateTimingStyles =
            flags.recalculateTimingStyles || isFirst;
        }

        maxDelayTime = maxDelay * ONE_SECOND;
        maxDurationTime = maxDuration * ONE_SECOND;
        if (!options.skipBlocking) {
          flags.blockTransition = timings.transitionDuration > 0;
          flags.blockKeyframeAnimation =
            timings.animationDuration > 0 &&
            stagger.animationDelay > 0 &&
            stagger.animationDuration === 0;
        }

        if (options.from) {
          if (options.cleanupStyles) {
            registerRestorableStyles(
              restoreStyles,
              node,
              Object.keys(options.from),
            );
          }
          applyAnimationFromStyles(element, options);
        }

        if (flags.blockTransition || flags.blockKeyframeAnimation) {
          applyBlocking(maxDuration);
        } else if (!options.skipBlocking) {
          blockTransitions(node, false);
        }

        // TODO(matsko): for 1.5 change this code to have an animator object for better debugging
        return {
          $$willAnimate: true,
          end: endFn,
          start() {
            if (animationClosed) return;

            runnerHost = {
              end: endFn,
              cancel: cancelFn,
              resume: null, // this will be set during the start() phase
              pause: null,
            };

            runner = new $$AnimateRunner(runnerHost);

            waitUntilQuiet(start);

            // we don't have access to pause/resume the animation
            // since it hasn't run yet. AnimateRunner will therefore
            // set noop functions for resume and pause and they will
            // later be overridden once the animation is triggered
            return runner;
          },
        };

        function endFn() {
          close();
        }

        function cancelFn() {
          close(true);
        }

        function close(rejected) {
          // if the promise has been called already then we shouldn't close
          // the animation again
          if (animationClosed || (animationCompleted && animationPaused))
            return;
          animationClosed = true;
          animationPaused = false;

          if (preparationClasses && !options.$$skipPreparationClasses) {
            element[0].classList.remove(...preparationClasses.split(" "));
          }
          activeClasses = pendClasses(preparationClasses, ACTIVE_CLASS_SUFFIX);
          if (activeClasses) {
            element[0].classList.remove(...activeClasses.split(" "));
          }

          blockKeyframeAnimations(node, false);
          blockTransitions(node, false);

          temporaryStyles.forEach((entry) => {
            // There is only one way to remove inline style properties entirely from elements.
            // By using `removeProperty` this works, but we need to convert camel-cased CSS
            // styles down to hyphenated values.
            node.style[entry[0]] = "";
          });

          applyAnimationClasses(element, options);
          applyAnimationStyles(element, options);

          if (Object.keys(restoreStyles).length) {
            Object.entries(restoreStyles).forEach(([prop, value]) => {
              if (value) {
                node.style.setProperty(prop, value);
              } else {
                node.style.removeProperty(prop);
              }
            });
          }

          // the reason why we have this option is to allow a synchronous closing callback
          // that is fired as SOON as the animation ends (when the CSS is removed) or if
          // the animation never takes off at all. A good example is a leave animation since
          // the element must be removed just after the animation is over or else the element
          // will appear on screen for one animation frame causing an overbearing flicker.
          if (options.onDone) {
            options.onDone();
          }

          if (events && events.length) {
            // Remove the transitionend / animationend listener(s)
            element.off(events.join(" "), onAnimationProgress);
          }

          // Cancel the fallback closing timeout and remove the timer data
          const animationTimerData = element.data(ANIMATE_TIMER_KEY);
          if (animationTimerData) {
            clearTimeout(animationTimerData[0].timer);
            element.removeData(ANIMATE_TIMER_KEY);
          }

          // if the preparation function fails then the promise is not setup
          if (runner) {
            runner.complete(!rejected);
          }
        }

        function applyBlocking(duration) {
          if (flags.blockTransition) {
            blockTransitions(node, duration);
          }

          if (flags.blockKeyframeAnimation) {
            blockKeyframeAnimations(node, !!duration);
          }
        }

        function closeAndReturnNoopAnimator() {
          runner = new $$AnimateRunner({
            end: endFn,
            cancel: cancelFn,
          });

          // should flush the cache animation
          waitUntilQuiet(() => {});
          close();

          return {
            $$willAnimate: false,
            start() {
              return runner;
            },
            end: endFn,
          };
        }

        function onAnimationProgress(event) {
          event.stopPropagation();
          const ev = event.originalEvent || event;

          if (ev.target !== node) {
            // Since TransitionEvent / AnimationEvent bubble up,
            // we have to ignore events by finished child animations
            return;
          }

          // we now always use `Date.now()` due to the recent changes with
          // event.timeStamp in Firefox, Webkit and Chrome (see #13494 for more info)
          const timeStamp = ev.$manualTimeStamp || Date.now();

          /* Firefox (or possibly just Gecko) likes to not round values up
           * when a ms measurement is used for the animation */
          const elapsedTime = parseFloat(
            ev.elapsedTime.toFixed(ELAPSED_TIME_MAX_DECIMAL_PLACES),
          );

          /* $manualTimeStamp is a mocked timeStamp value which is set
           * within browserTrigger(). This is only here so that tests can
           * mock animations properly. Real events fallback to event.timeStamp,
           * or, if they don't, then a timeStamp is automatically created for them.
           * We're checking to see if the timeStamp surpasses the expected delay,
           * but we're using elapsedTime instead of the timeStamp on the 2nd
           * pre-condition since animationPauseds sometimes close off early */
          if (
            Math.max(timeStamp - startTime, 0) >= maxDelayTime &&
            elapsedTime >= maxDuration
          ) {
            // we set this flag to ensure that if the transition is paused then, when resumed,
            // the animation will automatically close itself since transitions cannot be paused.
            animationCompleted = true;
            close();
          }
        }

        function start() {
          if (animationClosed) return;
          if (!node.parentNode) {
            close();
            return;
          }

          // even though we only pause keyframe animations here the pause flag
          // will still happen when transitions are used. Only the transition will
          // not be paused since that is not possible. If the animation ends when
          // paused then it will not complete until unpaused or cancelled.
          const playPause = function (playAnimation) {
            if (!animationCompleted) {
              animationPaused = !playAnimation;
              if (timings.animationDuration) {
                const value = blockKeyframeAnimations(node, animationPaused);
                if (animationPaused) {
                  temporaryStyles.push(value);
                } else {
                  removeFromArray(temporaryStyles, value);
                }
              }
            } else if (animationPaused && playAnimation) {
              animationPaused = false;
              close();
            }
          };

          // checking the stagger duration prevents an accidentally cascade of the CSS delay style
          // being inherited from the parent. If the transition duration is zero then we can safely
          // rely that the delay value is an intentional stagger delay style.
          const maxStagger =
            itemIndex > 0 &&
            ((timings.transitionDuration && stagger.transitionDuration === 0) ||
              (timings.animationDuration && stagger.animationDuration === 0)) &&
            Math.max(stagger.animationDelay, stagger.transitionDelay);
          if (maxStagger) {
            setTimeout(
              triggerAnimationStart,
              Math.floor(maxStagger * itemIndex * ONE_SECOND),
              false,
            );
          } else {
            triggerAnimationStart();
          }

          // this will decorate the existing promise runner with pause/resume methods
          runnerHost.resume = function () {
            playPause(true);
          };

          runnerHost.pause = function () {
            playPause(false);
          };

          function triggerAnimationStart() {
            // just incase a stagger animation kicks in when the animation
            // itself was cancelled entirely
            if (animationClosed) return;

            applyBlocking(false);

            temporaryStyles.forEach((entry) => {
              const key = entry[0];
              const value = entry[1];
              node.style[key] = value;
            });

            applyAnimationClasses(element, options);
            element[0].classList.add(
              ...activeClasses.split(" ").filter((x) => x !== ""),
            );
            if (flags.recalculateTimingStyles) {
              cacheKey = $$animateCache.cacheKey(
                node,
                method,
                options.addClass,
                options.removeClass,
              );

              timings = computeTimings(node, cacheKey, false);
              relativeDelay = timings.maxDelay;
              maxDelay = Math.max(relativeDelay, 0);
              maxDuration = timings.maxDuration;

              if (maxDuration === 0) {
                close();
                return;
              }

              flags.hasTransitions = timings.transitionDuration > 0;
              flags.hasAnimations = timings.animationDuration > 0;
            }

            if (flags.applyAnimationDelay) {
              relativeDelay =
                typeof options.delay !== "boolean" &&
                truthyTimingValue(options.delay)
                  ? parseFloat(options.delay)
                  : relativeDelay;

              maxDelay = Math.max(relativeDelay, 0);
              timings.animationDelay = relativeDelay;
              delayStyle = getCssDelayStyle(relativeDelay, true);
              temporaryStyles.push(delayStyle);
              node.style[delayStyle[0]] = delayStyle[1];
            }

            maxDelayTime = maxDelay * ONE_SECOND;
            maxDurationTime = maxDuration * ONE_SECOND;

            if (options.easing) {
              let easeProp;
              const easeVal = options.easing;
              if (flags.hasTransitions) {
                easeProp = TRANSITION_PROP + TIMING_KEY;
                temporaryStyles.push([easeProp, easeVal]);
                node.style[easeProp] = easeVal;
              }
              if (flags.hasAnimations) {
                easeProp = ANIMATION_PROP + TIMING_KEY;
                temporaryStyles.push([easeProp, easeVal]);
                node.style[easeProp] = easeVal;
              }
            }

            if (timings.transitionDuration) {
              events.push(TRANSITIONEND_EVENT);
            }

            if (timings.animationDuration) {
              events.push(ANIMATIONEND_EVENT);
            }

            startTime = Date.now();
            const timerTime =
              maxDelayTime + CLOSING_TIME_BUFFER * maxDurationTime;
            const endTime = startTime + timerTime;

            const animationsData = element.data(ANIMATE_TIMER_KEY) || [];
            let setupFallbackTimer = true;
            if (animationsData.length) {
              const currentTimerData = animationsData[0];
              setupFallbackTimer = endTime > currentTimerData.expectedEndTime;
              if (setupFallbackTimer) {
                clearTimeout(currentTimerData.timer);
              } else {
                animationsData.push(close);
              }
            }

            if (setupFallbackTimer) {
              const timer = setTimeout(onAnimationExpired, timerTime, false);
              animationsData[0] = {
                timer,
                expectedEndTime: endTime,
              };
              animationsData.push(close);
              element.data(ANIMATE_TIMER_KEY, animationsData);
            }

            if (events.length) {
              element.on(events.join(" "), onAnimationProgress);
            }

            if (options.to) {
              if (options.cleanupStyles) {
                registerRestorableStyles(
                  restoreStyles,
                  node,
                  Object.keys(options.to),
                );
              }
              applyAnimationToStyles(element, options);
            }
          }

          function onAnimationExpired() {
            const animationsData = element.data(ANIMATE_TIMER_KEY);

            // this will be false in the event that the element was
            // removed from the DOM (via a leave animation or something
            // similar)
            if (animationsData) {
              for (let i = 1; i < animationsData.length; i++) {
                animationsData[i]();
              }
              element.removeData(ANIMATE_TIMER_KEY);
            }
          }
        }
      };
    },
  ];
}

function blockTransitions(node, duration) {
  // we use a negative delay value since it performs blocking
  // yet it doesn't kill any existing transitions running on the
  // same element which makes this safe for class-based animations
  const value = duration ? `-${duration}s` : "";
  applyInlineStyle(node, [TRANSITION_DELAY_PROP, value]);
  return [TRANSITION_DELAY_PROP, value];
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy