JavaScript添加/删除动画类,仅执行一次动画。

4
尝试构建一个时间选择器,它应该在输入更改时进行动画处理。我想通过从CSS中添加和删除动画类来实现这一点。问题是,它只在第一次起作用。
我尝试设置一个间隔开始,以在一定的时间后(略高于动画持续时间)删除类,但这会使每次单击的动画时间缩短,并且到第三或第四次单击时,它将不再进行动画处理。
然后我选择检查类是否存在,首先删除它,然后再添加它。确保每次都从头开始。但现在它只在第一次进行动画处理。 Codepen HTML:

    <div id="wrapper">
      <div id="uphour"></div>
      <div id="upminute"></div>
      <div id="upampm"></div>
      <div class="animated">
        <div id="hour" data-hour="1">2</div>
      </div>
      <div class="animated">
        <div id="minute" data-minute="1">50</div>
      </div>
      <div class="animated">
        <div id="ampm" data-minute="AM">AM</div>
      </div>
      <div id="downhour"></div>
      <div id="downminute"></div>
      <div id="downampm"></div>
      </div>


这里的数据属性目前没有使用。
SCSS:

    $finger: 2cm;
    $smallfinger: .3cm;

    #wrapper {
      display: grid;
      grid-template-columns: $finger $finger $finger;
      grid-template-rows: $finger $finger $finger;
      grid-column-gap: $smallfinger;
      grid-row-gap: $smallfinger;
      position:absolute;
      width: 100vw;
      background: #FF00FF;
      height: calc( (#{$finger} * 3) + (#{$smallfinger} * 4) );
      box-sizing: border-box;
      bottom:$finger;
      left:0;
      padding:0;
      margin:0;
      align-content: center;
      justify-content: center;
      }

    #uphour,
    #upminute,
    #upampm,
    #downhour,
    #downminute,
    #downampm {
      display: flex;
      background: rgba(255,0,0,.4);
      width: $finger;
      height: $finger;
      margin-left: 1vw;
      margin-right: 1vw;
      border: 1px solid black;
    }

    .animated {
      display: flex;
      width: $finger;
      height: $finger;
      margin-left: 1vw;
      margin-right: 1vw;
      border: 1px solid black;
      overflow:hidden;
    }


    #minute, #hour, #ampm {
      display: flex;
      /*background: rgba(255,0,0,.4); for testing of animation only*/
      width: $finger;
      height: $finger;
      border: 0;
      position: relative;
      align-items: center;
      justify-content: center;
      font-size: .8cm;
    }

    .animateStart {
      -webkit-animation-name: downandout; /* Safari 4.0 - 8.0 */
      animation-name: downandout;
      -webkit-animation-duration: 250ms; /* Safari 4.0 - 8.0 */
      animation-duration: 250ms;
      /*-webkit-animation-iteration-count: infinite;
      animation-iteration-count: infinite;*/
      transition-timing-function: cubic-bezier(1,0,0,1);
    }

    /* Safari 4.0 - 8.0 */
    @-webkit-keyframes downandout {
      0%   {top: 0; left: 0;}
      10%  {top: 100%; left:0;}
      50%  {top: 100%; left: 100%;}
      90% {top: -100%; left:100%;}
    }

    /* Standard syntax */
    @keyframes downandout {
      0%   {top: 0; left: 0;}
      40%  {top: 100%; left:0;}
      42%  {top: 100%; left: calc(#{$finger} * 2);}
      48% {top: -110%; left: calc(#{$finger} * 2);}
      50% {top: -110%; left:0;}
      100% {top: 0; left:0;}
    }

Javascript:


    var uphourEl = document.getElementById("uphour");
    var downhourEl = document.getElementById("downhour");
    var upminuteEl = document.getElementById("upminute");
    var downminuteEl = document.getElementById("downminute");
    var upampmEl = document.getElementById("upampm");
    var downampmEl = document.getElementById("downampm");

    uphourEl.addEventListener("click", increaseHour);
    downhourEl.addEventListener("click", decreaseHour);
    upminuteEl.addEventListener("click", increaseMinute);
    downminuteEl.addEventListener("click", decreaseMinute);
    upampmEl.addEventListener("click", swapAMPM);
    downampmEl.addEventListener("click", swapAMPM);

    var hourEl = document.getElementById("hour"); 
    var minuteEl = document.getElementById("minute"); 
    var ampmEl = document.getElementById("ampm"); 

    function increaseHour() {
      let value = hourEl.innerHTML;
      if (value > 11) {
          value = 1
      } else  {
        value++;
      }
      hourEl.innerHTML = value;
      console.log(value);
    }

    function decreaseHour() {
      let value = hourEl.innerHTML;
      if (value < 2) {
          value = 12
      } else  {
        value--;
      }
      hourEl.innerHTML = value;
      console.log(value);
    }

    function increaseMinute() {
      let value = minuteEl.innerHTML;
      if (value > 58) {
          value = 0
      } else  {
        value++;
      }
      minuteEl.innerHTML = value;
      console.log(value);
    }

    function decreaseMinute() {
      let value = minuteEl.innerHTML;
      if (value < 1) {
          value = 59
      } else  {
        value--;
      }
      minuteEl.innerHTML = value;
      console.log(value);
    }

    function swapAMPM() {
      let value = ampmEl.innerHTML;
      if (ampmEl.hasAttribute("class")) {
        ampmEl.removeAttribute("class");
      }
      ampmEl.setAttribute("class", "animateStart");
      if (value === "AM") {
          value = "PM";
          ampmEl.innerHTML = value;
          console.log("Changed from AM");
      } else  {
        value = "AM";
        ampmEl.innerHTML = value;
        console.log("Changed from PM");
      }
    }

目前我无法使JavaScript末尾的swapAMPM函数按预期运行。

您对我遇到这种情况有什么建议吗?我是否缺少某些最佳实践?

4个回答

2
您可以使用动画迭代事件侦听器来检测动画何时结束,并正确地删除您的类。为了使其起作用,动画迭代必须设置为无限。在我的更改中,我还利用元素的classList属性添加/删除元素的类。
注意:如果没有至少1次迭代,则动画迭代事件将永远不会触发,因此代码将无法工作!
编辑:classList的浏览器支持相当近期,因此,如果您需要支持旧版浏览器,则可以退回到其他解决方案,或添加一个classList polyfill。
首先,我们需要一个函数来检测浏览器支持哪个动画迭代事件:
function whichAnimationEvent(){
  var el = document.createElement("fakeelement");

  var animations = {
    "animation"      : "animationiteration",
    "OAnimation"     : "oAnimationIteration",
    "MozAnimation"   : "animationiteration",
    "WebkitAnimation": "webkitAnimationIteration"
  };

  for (let t in animations){
    if (el.style[t] !== undefined){
      return animations[t];
    }
  }
}

接下来,我们需要为该事件添加一个事件监听器,当迭代触发时,从元素的 classList 中移除该类,如下所示:
ampmEl.addEventListener(whichAnimationEvent(),function(){
  console.log('ampmEl event listener fired')
  ampmEl.classList.remove('animateStart');
});

接下来,我们将更改swapAMPM函数,使用元素classListadd方法在执行交换之前添加类,以便进行动画效果。
function swapAMPM() {
  let value = ampmEl.innerHTML;
  ampmEl.classList.add('animateStart');
  if (value === "AM") {
      value = "PM";
      ampmEl.innerHTML = value;
      console.log("Changed from AM");
  } else  {
    value = "AM";
    ampmEl.innerHTML = value;
    console.log("Changed from PM");
  }
}

最后,我们需要更新 CSS,将动画迭代设置为“无限”,这样我们的事件才会触发。
.animateStart {
  -webkit-animation-name: downandout; /* Safari 4.0 - 8.0 */
  animation-name: downandout;
  -webkit-animation-duration: 250ms; /* Safari 4.0 - 8.0 */
  animation-duration: 250ms;
  -webkit-animation-iteration-count: infinite;
  animation-iteration-count: infinite;
  transition-timing-function: cubic-bezier(1,0,0,1);
}

完整的工作示例 codepen

1
太棒了!昨天我碰巧就发现了这个解决方案,但我从未理解fakeelement部分,而我找到的文章也没有提到无限迭代以及为什么需要它,也没有像你所做的那样进行详细的分解。也许现在我有机会理解它了。你能否尝试解释一下fakeelement部分?如果可以的话,我会很感激!我将把这标记为解决方案,因为它似乎是长期可行的解决方案。 - Streching my competence
我们创建一个虚假元素,以便检查其样式属性,以查看它本地具有哪些动画属性,从而揭示它支持哪些动画迭代事件。如果使用命名元素,例如aimgdiv等,则属性可能存在也可能不存在,但是任何新定义的元素默认都具有所有支持的属性,因此这可能是为什么我们使用“fakeelement”的原因。 - r3wt
@Strechingmycompetence,我在答案中添加了一个关于classList浏览器支持的注释。我之前忘记提到旧版浏览器不支持它。 - r3wt
好的!那真的很有帮助! - Streching my competence

2
我认为这是在vanilla中完成的方法。
gugateider是正确的,你需要一个setTimeout,虽然有AnimationEvent API,但它并不完全支持。
以下是简化的方法:
无需事件监听器或收集ID
更新:
  • 包括防抖以防止多次点击同一按钮
  • 调整了超时时间(超时时间是添加的,以便值在视图之外更改,从而产生数字实际上正在旋转的错觉,AnimationEvent无法检测您是否处于动画的一半。)
<div class="wrapper">

  <div id="hour" class="unit">
    <div class="plus button" onClick="pressedButton(event);">+</div>
    <div class="value"><div>10</div></div>
    <div class="minus button" onClick="pressedButton(event);">-</div>
  </div>

    <div id="minute" class="unit">
    <div class="plus button"  onClick="pressedButton(event);">+</div>
      <div class="value"><div>36</div></div>
    <div class="minus button"  onClick="pressedButton(event);">-</div>
  </div>

    <div id="meridiem" class="unit">
    <div class="plus button"  onClick="pressedButton(event);">+</div>
      <div class="value"><div>AM</div></div>
    <div class="minus button"  onClick="pressedButton(event);">-</div>
  </div>



</div>


.wrapper {
  display: flex;
  flex-flow: row no-wrap;
  justify-content: space-between;
  align-items: center;
  width: 200px;
  height: 200px;
  margin: 100px auto;
  background: red;
  padding: 20px;
}

.unit {
  display: flex;
  flex-flow: column;
  justify-content: space-between;
  align-items: space-between;
  height: 100%;
  position: relative;
}

.button {
  border: 2px solid black;
  height: 50px;
  width: 50px;
  cursor: pointer;
  display: flex;
  justify-content: center;
  align-items: center;
  background: grey;

  &:hover {
    opacity: 0.8;
  }

}


.value {
  border: 2px solid black;
  height: 50px;
  width: 50px;
  font-family: sans-serif;
  font-size: 20px;
  display: flex;
  justify-content: center;
  align-items: center;
  background: lightgrey;
    overflow: hidden;
}

.animate {
  top: 0;
  position: relative;
  overflow: hidden;
  animation-name: downandout;
  animation-duration: 1s;
  animation-iteration-count: 1;
  transition-timing-function: ease-in-out;

  &--reverse {
    animation-direction: reverse;
  }
}




 @keyframes downandout {
   0%  {top: 0}
   50% {top: 50px}
   51% {top: -50px}
   100%  {top: 0}
}

debounce = function(func, wait, immediate) {
  var timeout;
  return function() {
    var context = this,
      args = arguments;
    var later = function() {
      timeout = null;
      if (!immediate) func.apply(context, args);
    };
    var callNow = immediate && !timeout;
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
    if (callNow) func.apply(context, args);
  };
}

pressedButton = function($event) {
  $event.preventDefault();

  // debouncedFn to stop multiple clicks to the same button
  var debouncedFn = debounce(function() {
    // All the taxing stuff you do
    const target = $event.target;
    const elm = target.parentNode.children[1];

    let direction;

    // clone the element to restart the animation
    const newone = elm.cloneNode(true);
    // add the first animate
    newone.children[0].classList.add("animate");

    // What button was pressed
    if (target.classList.contains("minus")) {
      direction = "down";
    } else {
      direction = "up";
    }

    // direction of animation
    if (direction === "down") {
      newone.children[0].classList.add("animate--reverse");
    } else {
      newone.children[0].classList.remove("animate--reverse");
    }

    // add the new element to the DOM
    elm.parentNode.replaceChild(newone, elm);

    // change value after half of the animation has completed
    setTimeout(function() {
      switch (target.parentNode.id) {
        case "hour":
        case "minute":
          if (direction === "down") {
            newone.children[0].innerText--;
          } else {
            newone.children[0].innerText++;
          }
          break;

        case "meridiem":
          if (newone.children[0].innerText === "PM") {
            newone.children[0].innerText = "AM";
          } else {
            newone.children[0].innerText = "PM";
          }
      }
    }, 100);
  }, 250);

  // Call my function
  debouncedFn();

};

https://codepen.io/eddy14u/pen/VwZJmdW


这是一个非常好的答案,因为它大大简化了代码,并允许使用相同的代码整洁地对所有元素进行动画处理。唯一的问题是,我会使用动画事件监听器而不是setTimeout - r3wt
1
不幸的是,由于超时问题,这个答案存在一个很大的缺陷。连续点击按钮会重置动画,因此多次点击只会增加一次,而不是像人们期望的那样增加按下的次数。 - r3wt

1

我知道在这里使用setTimeout并不是最好的选择,但既然你已经基本完成了这个功能,你可以尝试使用下面的代码替换swapAMPM函数:

function swapAMPM() {
 let value = ampmEl.innerHTML;
 if (ampmEl.hasAttribute("class")) {
     ampmEl.removeAttribute("class");
 }
 setTimeout( () => { 
     ampmEl.setAttribute("class", "animateStart");
     if (value === "AM") {
      value = "PM";
      ampmEl.innerHTML = value;
      console.log("Changed from AM");
     } else  {
        value = "AM";
        ampmEl.innerHTML = value;
        console.log("Changed from PM");
    } 
 }, 150);
}

不错!最终我尝试在每个条件结果周围设置setTimeout,因为我不知道可以将整个条件包装起来,但这样代码更少,所以我会采用这种方式!此外,我还在第一个条件周围添加了一个500毫秒的setTimeout,而第二个条件中的setTimeout现在设置为300。这样,被动画化的div在更改之前就已经离开了视觉空间,并且在再次进入视觉空间时已经更改。 - Streching my competence

0
在你的函数swapAMPM中的if语句中,你试图移除一个属性,这并没有太多意义,因为你立刻又将其设置回来。
if (ampmEl.hasAttribute("class")) {
  ampmEl.removeAttribute("class");
}
ampmEl.setAttribute("class", "animateStart")

一个快速的解决方法是在动画完成后设置超时来移除类:
function swapAMPM() {
  let value = ampmEl.innerHTML;
  ampmEl.classList.add("animateStart");
  setTimeout(() => { 
    ampmEl.classList.remove("animateStart") 
  }, 260)
  if (value === "AM") {
      value = "PM";
      ampmEl.innerHTML = value;
      console.log("Changed from AM");
  } else  {
    value = "AM";
    ampmEl.innerHTML = value;
    console.log("Changed from PM");
  }
}

这是我从一开始尝试的方法,每次按下交换按钮时,动画时间都会减少到零。虽然我不知道为什么..但应该在更改方法之前保存确切的示例,以便我可以复制它。 - Streching my competence
我已经检查过了,它没有任何问题。虽然这只是一个快速而不太可靠的修复方法。r3wt 绝对有一个可靠的解决方案。 - maxim

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接