Search code examples
javascriptcssinternet-explorercss-animationsinternet-explorer-11

deleteRule CSSKeyframesRule method confusing behaviour in IE11


I have created basic animation of a circle using css @keyframes. I'm using javascript to trigger animation start/stop by click inside the circle.

The animation itself could be divided into 5 (looped) phases:
pause-expand-pause-shrink-pause (see @keyframes css section below)

The goal I want to achieve is, eventually, to be able to set animation duration and change the values of keyframes (say by having input fields for pause and expand/shrink durations - details doesn't really matter for the scope of this question). I have put together a JavaScript function, to perform this task, and have set it to onload just to test how it works.

My HTML:

<!doctype html>
<html>
    <head>
        <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
        <meta content="utf-8" http-equiv="encoding">

        <link rel="stylesheet" href="style.css">
        <script src = "animation.js"></script>

    </head>
    <body onload=setAnimationDuration(1,1)>
        <div id="circle" class='circle-paused' onclick=cssAnimation()></div>
    </body>
</html>

My CSS:

#circle {
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
}

.circle-paused {
width: 9%;
padding-top: 9%;
border-radius: 50%;
background-color: #800080;
margin: auto;
}

.circle-animated {
/* 2 sec pause 4 sec expand_shrink*/
width: 9%;
padding-top: 9%;
-webkit-animation-name: my-circle; /* Safari 4.0 - 8.0 */
-webkit-animation-duration: 12s; /* Safari 4.0 - 8.0 */
animation-name: my-circle;
animation-duration: 12s;
animation-iteration-count: infinite;
animation-timing-function: linear;
border-radius: 50%;
margin: auto;
}

@keyframes my-circle {
0% {background-color: #800080; width: 9%; padding-top: 9%;}
33.3% {background-color: #D8BFD8; width: 28%; padding-top: 28%;}
50% {background-color: #D8BFD8; width: 28%; padding-top: 28%;}
83.3% {background-color: #800080; width: 9%; padding-top: 9%;}
100% {background-color: #800080; width: 9%; padding-top: 9%;}
}

My JavaScript:

function cssAnimation() {
  if (document.getElementById('circle').className == 'circle-paused') {
    document.getElementById('circle').className = 'circle-animated'
  } else {
    document.getElementById('circle').className = 'circle-paused'
  }
}


function findKeyframes(animation_name) {
  // get list of current keyframe rules
  var style_sheet = document.styleSheets;
  for (var i = 0; i < style_sheet.length; ++i) {
    for (var j = 0; j < style_sheet[i].cssRules.length; ++j) {
      // type 7 correspond to CSSRule.KEYFRAMES_RULE, for more info see https://developer.mozilla.org/en-US/docs/Web/API/CSSRule 
      if (style_sheet[i].cssRules[j].type == 7 && style_sheet[i].cssRules[j].name == animation_name) {
        return style_sheet[i].cssRules[j];
      }
    }
  }
  // keyframe rules were not found for given animation_name
  return null;
}


function getPercentage(total, fraction) {
  // Returns what percentage the fraction is from total
  // The result is rounded to 1 decimal place
  return Math.round(((100 / total) * fraction) * 10) / 10;
}


function setAnimationDuration(pause, expand_shrink) {
  var total_animation_duration = (pause * 2) + (expand_shrink * 2)
  var pause_percentage = getPercentage(total_animation_duration, pause)
  var expand_shrink_percentage = getPercentage(total_animation_duration, expand_shrink)

  var pause1 = pause_percentage + expand_shrink_percentage;
  var shrink = pause1 + expand_shrink_percentage;

  var frame_percentage_list = [0, expand_shrink_percentage, pause1, shrink, 100]

  var key_frame_list = findKeyframes('my-circle')
  var new_rule_list = []
  var to_be_removed_key_list = []

  //create array of new rules to be inserted
  //collecting old keys of rules to be deleted
  for(var i = 0; i < key_frame_list.cssRules.length; i++) {
    var current_rule = key_frame_list.cssRules[i].cssText

    to_be_removed_key_list.push(key_frame_list.cssRules[i].keyText)
    new_rule_list.push(current_rule.replace(/[+-]?([0-9]*[.])?[0-9]+%/, frame_percentage_list[i] + '%'))
  }

  // delete old rules
  for(var i = 0; i < to_be_removed_key_list.length; i++) {
    key_frame_list.deleteRule(to_be_removed_key_list[i])
  }

  // populate new ruels
  for(var i = 0; i < new_rule_list.length; i++) {
    key_frame_list.appendRule(new_rule_list[i])
  }


  document.getElementById('circle').style.animationDuration = total_animation_duration + "s"
}

Code above, on JSFiddle

The problem itself:
The code is working as expected in FireFox (55.0.3), Chrome (61.0) and Safari (11.0). Though, when I started testing it in IE11, I have found that key_frame_list.deleteRule('rule_key') throws an Invalid argument error. While researching the issue, I have found (and went trough) this article (though it does not tackle the IE problem, it improved my overall understanding of the css animations). On MSDN I have find two references, concerning deleteRule: one and two. Though I didn't really understood what was meant, in the second one, by:

The key must resolve to a number between 0 and 1, or the rule is ignored.

I assumed that in IE you have to pass index to deleteRule instead of string key. So I've tried to check my assumption in IE console. Here is what I have found (given my js code is in onload):

var key_frame_list = findKeyframes('my-circle')
key_frame_list.cssRules.length => 5
key_frame_list.deleteRule(0)
key_frame_list.cssRules.length => 4
key_frame_list.deleteRule(1)
key_frame_list.cssRules.length => 3
key_frame_list.deleteRule(0)
key_frame_list.deleteRule(1)
key_frame_list.deleteRule(2)
...
key_frame_list.cssRules.length => 3

What is happening is:

key_frame_list.deleteRule(0) - removes first rule (which is 0%)
key_frame_list.deleteRule(1) - removes the last rule (which is 100%)
After that no matter which index I pass to key_frame_list.deleteRule() the key_frame_list.cssRules.length remains 3.
My expectation was that I will be able to recur with key_frame_list.deleteRule(0) and remove all the rules (as I expected that the indexes will shift after each rule deletion).

Now, I would like to understand:

  1. What is the proper way (basically, 'Am I doing something wrong?') to use deleteRule in the IE (or if another method should be used)?
  2. Why am I not able to delete more than two rules out of five?
  3. Is there a method suitable for this purposes that will work with the same arguments on Firefox, Chrome and IE11, I am not aware of?

Solution

    1. What is the proper way (basically, 'Am I doing something wrong?') to use deleteRule in the IE (or if another method should be used)?

    2. Why am I not able to delete more than two rules out of five?

      The first MSDN link does not apply; that deleteRule() method applies to top-level rules, not keyframe rules.

      The text "The key must resolve to a number between 0 and 1, or the rule is ignored." from the second link is actually taken from the 2013 WD of css-animations, and means that instead of a string containing the 0%-100% keyframe selector, Internet Explorer expects a decimal number representing the percentage. The argument does not represent an index.

      So for a 0% keyframe rule, IE expects the value 0; for a 100% keyframe rule, IE expects the value 1; and for a 33.3% keyframe rule, IE expects the floating-point value 0.333:

      key_frame_list.deleteRule(0)     // Deletes the 0% keyframe rule
      key_frame_list.deleteRule(0.333) // Deletes the 33.3% keyframe rule
      key_frame_list.deleteRule(1)     // Deletes the 100% keyframe rule
      

      Once a 0% rule has been deleted, if no 0% rules remain then additional calls to deleteRule(0) will do nothing.

      And since keyframes cannot exceed 100%, deleteRule(2) is meaningless since it would mean deleting a 200% keyframe rule, which cannot exist.

    3. Is there a method suitable for this purposes that will work with the same arguments on Firefox, Chrome and IE11, I am not aware of?

      No; Internet Explorer 11 follows the 2013 WD (itself having been developed between 2012 and 2013 following the release of Internet Explorer 10), meaning its implementation is not consistent with the current standard, in which the deleteRule() method has been changed to accept a string argument instead of a numeric argument.

      This means the API is incompatible, and so there is no clean workaround. You'll just have to attempt both arguments. I changed the following statement in your fiddle:

      // delete old rules
      for(var i = 0; i < to_be_removed_key_list.length; i++) {
        key_frame_list.deleteRule(to_be_removed_key_list[i])
      }
      

      to:

      // delete old rules
      for(var i = 0; i < to_be_removed_key_list.length; i++) {
        try {
          key_frame_list.deleteRule(to_be_removed_key_list[i])
        } catch (e) {
          key_frame_list.deleteRule(+(parseFloat(to_be_removed_key_list[i]) / 100).toFixed(3))
        }
      }
      

      The +(parseFloat(to_be_removed_key_list[i]) / 100).toFixed(3) bit converts a percentage string to a numeric value taking rounding errors into account. The rounding errors inherent to IEEE-754 floating-point numbers is the reason the API was changed in the first place (that, and consistency with the appendRule() method which has always expected a string), except since it was only changed some time after Internet Explorer 11 was released, and since IE11 will no longer receive platform updates, this means IE11 is stuck with its old WD implementation (which, I must emphasize, was current at the time of its development).