<commit_before>import { setTransitionStyle, removeTransitionStyle } from './util.js';

const DEFAULT_EASING = 'cubic-bezier(.4,0,.22,1)';

/** @typedef {import('./animations.js').AnimationProps} AnimationProps */

/**
 * Runs CSS transition.
 */
class CSSAnimation {
  /**
   * onComplete can be unpredictable, be careful about current state
   *
   * @param {AnimationProps} props
   */
  constructor(props) {
    this.props = props;
    const {
      target,
      onComplete,
      transform,
      onFinish
      // opacity
    } = props;

    let {
      duration,
      easing,
    } = props;

    /** @type {() => void} */
    this.onFinish = onFinish;

    // support only transform and opacity
    const prop = transform ? 'transform' : 'opacity';
    const propValue = props[prop];

    /** @private */
    this._target = target;
    /** @private */
    this._onComplete = onComplete;

    duration = duration || 333;
    easing = easing || DEFAULT_EASING;

    /** @private */
    this._onTransitionEnd = this._onTransitionEnd.bind(this);

    // Using timeout hack to make sure that animation
    // starts even if the animated property was changed recently,
    // otherwise transitionend might not fire or transiton won't start.
    // https://drafts.csswg.org/css-transitions/#starting
    //
    // ¯\_(ツ)_/¯
    /** @private */
    this._firstFrameTimeout = setTimeout(() => {
      setTransitionStyle(target, prop, duration, easing);
      this._firstFrameTimeout = setTimeout(() => {
        target.addEventListener('transitionend', this._onTransitionEnd, false);
        target.addEventListener('transitioncancel', this._onTransitionEnd, false);
        target.style[prop] = propValue;
      }, 30); // Do not reduce this number
    }, 0);
  }

  /**
   * @private
   * @param {TransitionEvent} e
   */
  _onTransitionEnd(e) {
    if (e.target === this._target) {
      this._finalizeAnimation();
    }
  }

  /**
   * @private
   */
  _finalizeAnimation() {
    if (!this._finished) {
      this._finished = true;
      this.onFinish();
      if (this._onComplete) {
        this._onComplete();
      }
    }
  }

  // Destroy is called automatically onFinish
  destroy() {
    if (this._firstFrameTimeout) {
      clearTimeout(this._firstFrameTimeout);
    }
    removeTransitionStyle(this._target);
    this._target.removeEventListener('transitionend', this._onTransitionEnd, false);
    this._target.removeEventListener('transitioncancel', this._onTransitionEnd, false);
    if (!this._finished) {
      this._finalizeAnimation();
    }
  }
}

export default CSSAnimation;<commit_msg>Add a fallback timer to transitionend/cancel events<commit_after>import { setTransitionStyle, removeTransitionStyle } from './util.js';

const DEFAULT_EASING = 'cubic-bezier(.4,0,.22,1)';

/** @typedef {import('./animations.js').AnimationProps} AnimationProps */

/**
 * Runs CSS transition.
 */
class CSSAnimation {
  /**
   * onComplete can be unpredictable, be careful about current state
   *
   * @param {AnimationProps} props
   */
  constructor(props) {
    this.props = props;
    const {
      target,
      onComplete,
      transform,
      onFinish
      // opacity
    } = props;

    let {
      duration,
      easing,
    } = props;

    /** @type {() => void} */
    this.onFinish = onFinish;

    // support only transform and opacity
    const prop = transform ? 'transform' : 'opacity';
    const propValue = props[prop];

    /** @private */
    this._target = target;
    /** @private */
    this._onComplete = onComplete;

    duration = duration || 333;
    easing = easing || DEFAULT_EASING;

    /** @private */
    this._onTransitionEnd = this._onTransitionEnd.bind(this);

    // Using timeout hack to make sure that animation
    // starts even if the animated property was changed recently,
    // otherwise transitionend might not fire or transiton won't start.
    // https://drafts.csswg.org/css-transitions/#starting
    //
    // ¯\_(ツ)_/¯
    /** @private */
    this._helperTimeout = setTimeout(() => {
      setTransitionStyle(target, prop, duration, easing);
      this._helperTimeout = setTimeout(() => {
        target.addEventListener('transitionend', this._onTransitionEnd, false);
        target.addEventListener('transitioncancel', this._onTransitionEnd, false);

        // Safari occasionally does not emit transitionend event
        // if element propery was modified during the transition,
        // which may be caused by resize or third party component,
        // using timeout as a safety fallback
        this._helperTimeout = setTimeout(() => {
          this._finalizeAnimation();
        }, duration + 500);
        target.style[prop] = propValue;
      }, 30); // Do not reduce this number
    }, 0);
  }

  /**
   * @private
   * @param {TransitionEvent} e
   */
  _onTransitionEnd(e) {
    if (e.target === this._target) {
      this._finalizeAnimation();
    }
  }

  /**
   * @private
   */
  _finalizeAnimation() {
    if (!this._finished) {
      this._finished = true;
      this.onFinish();
      if (this._onComplete) {
        this._onComplete();
      }
    }
  }

  // Destroy is called automatically onFinish
  destroy() {
    if (this._helperTimeout) {
      clearTimeout(this._helperTimeout);
    }
    removeTransitionStyle(this._target);
    this._target.removeEventListener('transitionend', this._onTransitionEnd, false);
    this._target.removeEventListener('transitioncancel', this._onTransitionEnd, false);
    if (!this._finished) {
      this._finalizeAnimation();
    }
  }
}

export default CSSAnimation;
{'fraction_non_alphanumeric': 0.13114168304448812, 'fraction_numerical': 0.004466678577809541, 'mean_word_length': 2.654046997389034, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 10, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7293043', 'n_tokens_mistral': 1639, 'n_tokens_neox': 1576, 'n_words': 375}