1
\$\begingroup\$

I'm trying to set up some code to first test if the Vanilla JavaScript .animate() API is Supported, then if it's not check if requestAnimationFrame is supported and if it's not fallback to either setTimeout, setInterval or setImmediate.

I've been reading this e-book on google Smashing Webkit, which says that its always best practice to check for feature support before actually implementing it, so I'm trying to move all my App Animations inside of the checks below, and then implement fallbacks for backwards compatibility and older browsers.

I'm not 100% sure if this has any security concerns as I'm not a web security expert nor am I an expert coder by any standards. I'm still trying to learn JS and am wondering if this code block could be made better, more secure or to run more optimally i.e. removing unnecessary if statements or reworking the code so it is less verbose.

    document.addEventListener("DOMContentLoaded",(()=> {
        // ::>>. Notes:: ......................
    // ::>>. A Handy Function to get the Browser Prefix ~
          // Gets the browser prefix
        var brsrPrefix;
        navigator.sayswho= (function(){
          var N = navigator.appName, ua = navigator.userAgent, tem;
          var M = ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
              if(M && (tem = ua.match(/version\/([\.\d]+)/i))!= null) M[2] = tem[1];
                 M = M? [M[1], M[2]]: [N, navigator.appVersion,'-?'];
                 M = M[0];
              if(M == "Chrome")  { brsrPrefix = "webkit"; }
              if(M == "Firefox") { brsrPrefix = "moz"; }
              if(M == "Safari")  { brsrPrefix = "webkit"; }
              if(M == "MSIE")    { brsrPrefix = "ms"; }
        })();
    // ::>>. A Handy Function to get the Browser Prefix ~
        try{
            if(window.animate){
                console.log('.animate() API is Supported')
                // My Current Animations will be in here.
            }
            if(window.requestAnimationFrame){
                console.log('RequestAF is Supported')
                // 1st fallback in here.
            } if(!window.requestAnimationFrame) {
                  window.requestAnimationFrame = window.setImmediate
            } else {
                let requestAnimationFrame= window.requestAnimationFrame ||
                                           window.mozRequestAnimationFrame ||
                                           window.webkitRequestAnimationFrame ||
                                           window.msRequestAnimationFrame ||
                                           function(callback) {
                                              return window.setTimeout(callback, 1000/60)
                                           };

                    console.log(requestAnimationFrame);
            }
        }       // ::>>. Closing Bracket for First Try Catch.............
        catch(err){
            console.log(err)
        }       // ::>>. Closing Bracket for First Catch.............

            // ::>>. RequestAnimation FramePolyFill
                (function() {
                    var lastTime = 0;
                    var vendors = ['webkit', 'moz', 'ms'];
                    for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
                        window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
                        window.cancelAnimationFrame =
                          window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];
                    }
                    if (!window.requestAnimationFrame)
                        window.requestAnimationFrame = function(callback, element) {
                            var currTime = new Date().getTime();
                            var timeToCall = Math.max(0, 16 - (currTime - lastTime));
                            var id = window.setTimeout(function() { callback(currTime + timeToCall); },
                              timeToCall);
                            lastTime = currTime + timeToCall;
                            return id;
                        };

                    if (!window.cancelAnimationFrame)
                        window.cancelAnimationFrame = function(id) {
                            clearTimeout(id);
                        };
                }());
            // ::>>. RequestAnimation FramePolyFill
}))

My concerns with the code is that I copied the first snippet from a blog post and I don't fully understand the RegEx that is used within it. I'm finding Regex very, very difficult to learn and is kinda confusing. Can anyone point out to me what tem and ua mean? I'm guessing ua is shorthand for user-agent.

Also, the code above is repeating itself in that its declaring var brsrPrefix & also var vendors = ['webkit', 'moz', 'ms']; Will look into trying to attempt to merge these two functions and compact the code as much as I can.

In my try catch if else statement it's checking to see if window.requestAnimationFrame is supported and then if it's not, its setting let requestAnimationFrame to be any of the browser specific versions. However, if I attempted to call or attach this let to an item it would end up being element.window.requestAnimationFrame which is the wrong syntax.

Also, Firefox Quantum supports the experimental .animate() API (not the jQuery version) but it is not console logging anything for this part, only logs that RAF is enabled.

Is this far too much code, just for doing a simple task such as checking browser support?

No errors in console so far. Can anyone help and review this and post meaningful comments so I can learn better and become a better developer?

ideally the code would test support for:

   -> .animate()
   -> .requestAnimationFrame()
   -> .setInterval()
   -> .setTimeout()
   -> .setImmediate 

in that Order, and Append Browser prefix Where necessary, but might have to research a whole lot more.

this post looks like it will be handy for reference Detecting_CSS_animation_support its has a similar implementation with Js, & Another Js Script on GitHub Similar Detect CSS Animation support and provide object of normalised properties

Here is a new question I asked with Similar Theme but A different Implementation to try and Achieve the same end goal, Using Switch statements instead:
Javascript Anime Support Switch Statement for .animate() Api

\$\endgroup\$
7
  • \$\begingroup\$ Still gonna wait for a little while before I decide to mark any answers as the right answer as I like to see what other people have to say aswell. not 100% I've got it down as the right approach yet. \$\endgroup\$
    – Ryan Stone
    Feb 23, 2020 at 19:00
  • 1
    \$\begingroup\$ "My concerns with the code is that I copied the first snippet from a blog post and I don't fully understand the RegEx that is used within it. I'm finding Regex very, very difficult to learn and is kinda confusing. Can anyone point out to me what tem and ua mean? guessing ua is shorthand for user-agent." - this is not on-topic for this site. "For licensing, moral, and procedural reasons, we cannot review code written by other programmers. We expect you, as the author, to understand why the code is written the way that it is." \$\endgroup\$ Mar 6, 2020 at 21:39
  • \$\begingroup\$ @Sᴀᴍ Onᴇᴌᴀ I do understand Why the code is wriiten the way it is, I understand also why regex has been used, but it is like an alien language to me & struggleing to learn it. \$\endgroup\$
    – Ryan Stone
    Mar 6, 2020 at 21:46
  • \$\begingroup\$ The Same as Javascript was when I first Started learning that \$\endgroup\$
    – Ryan Stone
    Mar 6, 2020 at 21:47
  • \$\begingroup\$ & Technically speaking All Code, ever written is or has been previously written by someone else... \$\endgroup\$
    – Ryan Stone
    Mar 6, 2020 at 21:50

3 Answers 3

4
\$\begingroup\$

Firstly, before I start, I'd like to mention that you might not need these JS animation functions. A lot of animations can be achieved through CSS transitions and keyframes, using JS to toggle classes where needed. In my opinion, JS should only step in when the animation becomes too intensive for CSS to handle.

There appears to be a lot of duplication here. I'm going to simplify a lot of it for these reasons:

  • requestAnimationFrame has great browser support and shouldn't need to be polyfilled. That said, I do still like consolidating the browser prefixed versions into one just to be on the safe side.
  • Most functionality is standardised so you shouldn't need to know the browser prefix.
  • Your support checks should exist separately to your animation code, meaning they can be reused for multiple animation blocks.

The support checks look like this (include at the top of your file):

// test for Web Animations API (.animate) support
// test from Modernizr.js
const WEB_ANIMATIONS_API_SUPPORTED = 'animate' in document.createElement('div');

// consolidate browser prefixed versions into one function
let requestAnimationFrame = (
    window.requestAnimationFrame ||
    window.mozRequestAnimationFrame ||
    window.webkitRequestAnimationFrame ||
    window.msRequestAnimationFrame ||
    function(callback) {
        return window.setTimeout(callback, 1000 / 60)
    }
);

Then when you want to create an animation, you can do:

if ( WEB_ANIMATIONS_API_SUPPORTED ) {
    // your .animate call
} else {
    requestAnimationFrame(function() {
        // your fallback function
    });
}

I'll also try and answer your other questions as best as I can here.

its always best practice to check for feature support before actually implementing it,

Absolutely correct and it's a very good practice to get into.

and then implement fallbacks for backwards compatibility and older browsers.

Again this is the best way to handle using new functionality. It's called progressive enhancement - use the lowest level technology to build a base experience and then enhance if the newer functions are available.

I'm not 100% sure if this has any security concerns

I can reliably say there are no security concerns here. If you want to know more about security in JS start by reading around XSS vulnerabilities.

Regex can be difficult to learn and even experienced devs struggle with it. I like to use a visual interface like https://regexr.com/ to see the matches in real time. You can also hover over each regex character to see what it means and how it interacts with characters before and after it.

Yes, ua is shorthand for User Agent, and tem appears to be short for temporary. It's only used to quickly hold the matches from the Regex. Personally, I hate this style of variable naming and always go for longer, self-documenting ones where possible.

As you mention the browser prefix is repeating itself. This is likely because the requestAnimationFrame polyfill you're using is a packaged polyfill, and comes with it's own browser prefix checking. If you were doing a lot of tests, then it would make sense to abstract this out to a separate browser prefixing function.

However, if I attempted to call or attach this let to an item it would end up being element.window.requestAnimationFrame which is the wrong syntax.

I'm not sure what you mean here. If you can give me more info, I'll try and provide an answer.

\$\endgroup\$
7
  • \$\begingroup\$ Tyler Thanks for the Answer, I will read through it fully in a short while, I'm trying to use Js instead of Pure Css As Css does not generally match the browsers refresh rate as far as I know, & you don't get as much control or customization through pure css. \$\endgroup\$
    – Ryan Stone
    Feb 19, 2020 at 1:38
  • \$\begingroup\$ Good response, much appreciated. Ideally I would like to move 'All' my animations into there own worker thread, and just have the browser support checks in my main .js file. But I'm still a far way away from learning all of that. \$\endgroup\$
    – Ryan Stone
    Feb 19, 2020 at 1:43
  • \$\begingroup\$ Do i need to include Modernizr.js for that test to work? \$\endgroup\$
    – Ryan Stone
    Feb 19, 2020 at 1:45
  • \$\begingroup\$ also the reasoning behind trying to use .animate() is because this is the newer version and is More Performant than requestAnimationFrame which in turn is Better suited to animation than Pure CSS. I understand your point that for basic non cpu/gpu animations I can just stick with Css, but the ones I was attempting are very Cpu intensive. Sorry for calling you Tyler. lol \$\endgroup\$
    – Ryan Stone
    Feb 19, 2020 at 2:10
  • \$\begingroup\$ No you don't need to include Modernizr for that - I extracted the relevant parts, so that'll run as a one-liner \$\endgroup\$ Feb 19, 2020 at 10:42
1
+200
\$\begingroup\$

Initial thoughts

Looking at the MDN documentation for Element.animate() I see this warning at the top:

This is an experimental technology
Check the Browser compatibility table carefully before using this in production.

Looking at that compatibility table we see it isn't supported at all by a couple browsers...

browser compatibility

I searched for "navigator.sayswho" online and found snippets like this gist that matches most of the first function, and I also see this gist for the requestAnimationFrame polyfill by Paul Irish. I read over the comments and noted the comment 7/21/2019 by jalbam claims to have an adaptation that has improved performance. I haven't tested it out but it may work slightly better than the original.

My conclusion is that you basically wrapped those two snippets in a DOM loaded callback (and perhaps modified the variable name browserPrefix to brsrPrefix.

"...am wondering if this code block could be made better, more secure or to run more optimally i.e. removing unnecessary if statements or reworking the code so it is less verbose"

It seems that the first snippet is pointless because:

  • nothing is returned from the IIFE that is used to assign navigator.sayswho and thus that is undefined, and
  • brsrPrefix doesn’t appear to be used and its scope is limited to the anonymous/lambda function/closure passed to the DOM loaded event handler.

document.addEventListener("DOMContentLoaded",(()=> {
        // ::>>. Notes:: ......................
    // ::>>. A Handy Function to get the Browser Prefix ~
          // Gets the browser prefix
        var brsrPrefix;
        navigator.sayswho= (function(){
          var N = navigator.appName, ua = navigator.userAgent, tem;
          var M = ua.match(/(opera|chrome|safari|firefox|msie)\/?\s*(\.?\d+(\.\d+)*)/i);
              if(M && (tem = ua.match(/version\/([\.\d]+)/i))!= null) M[2] = tem[1];
                 M = M? [M[1], M[2]]: [N, navigator.appVersion,'-?'];
                 M = M[0];
              if(M == "Chrome")  { brsrPrefix = "webkit"; }
              if(M == "Firefox") { brsrPrefix = "moz"; }
              if(M == "Safari")  { brsrPrefix = "webkit"; }
              if(M == "MSIE")    { brsrPrefix = "ms"; }
        })();
        
        console.log(' inside DOM Loaded callback - brsrPrefix', brsrPrefix, 'navigator.sayswho: ', navigator.sayswho);
}))
setTimeout(function() {
console.log(' outside DOM Loaded callback - brsrPrefix', brsrPrefix, 'navigator.sayswho: ', navigator.sayswho);
}, 3000);

This hackernoon article about polyfills and transpilers might likely be interesting.

Suggestions

Avoid Es6 features when attempting to target older browsers

The first thing I noticed is that the first line contains an arrow function expression:

 document.addEventListener("DOMContentLoaded",(()=> {

Bear in mind the browser support for those- some browsers that wouldn’t support Element.animate() but would support requestAnimationFrame() would not support the syntax of an arrow function.

I tried running the code as is in IE 11 but set to emulate version 10 (both in Document mode and user agent string) since version 10 is the earliest version to support requestAnimationFrame().

emulation mode in IE 11

It showed a syntax error in the console:

syntax error in IE 10 emulation

There is another es6 feature that leads to an error in IE version 10 and earlier: the let keyword is used:

            let requestAnimationFrame= window.requestAnimationFrame ||
                                       window.mozRequestAnimationFrame ||
                                       window.webkitRequestAnimationFrame ||
                                       window.msRequestAnimationFrame ||
                                       function(callback) {
                                          return window.setTimeout(callback, 1000/60)
                                       };

Note the browser support for that keyword. So use a traditional function expression instead of an arrow function and the var keyword instead of let.

Users can modify the User agent of their browser

As described above with the IE 10/11 test, users can modify the user agent - including a completely different vendor - this post describes how to change the user agent in Chrome, Firefox and Safari. Because of this it is best to limit dependence on detection using the user agent string.

Browser support for addEventListener()

Another thing to consider is the browser support for addEventListener()

In Internet Explorer versions before IE 9, you have to use attachEvent(), rather than the standard addEventListener(). For IE, we modify the preceding example to:

if (el.addEventListener) {
  el.addEventListener('click', modifyText, false); 
} else if (el.attachEvent)  {
  el.attachEvent('onclick', modifyText);
}

2

If you want to support those versions of IE then you would likely want to modify the code to add the DOM-loaded callback accordingly. Note that whle IE supports DOMContentLoaded starting with version 93, events tied to that event don't always get triggered in IE. You may have to do something like this:

function checkBrowser() {
    // code currently in the anonymous callback to ocument.addEventListener("DOMContentLoaded"
}

// in case the document is already rendered
if (document.readyState!='loading') checkBrowser();
// modern browsers
else if (document.addEventListener) document.addEventListener('DOMContentLoaded', checkBrowser);
// IE <= 8
else document.attachEvent('onreadystatechange', function(){
    if (document.readyState=='complete') checkBrowser();
});

-from plainJS.com

Are you sure that code needs to be executed after the DOM has loaded? if not, it could be put into an IIFE to preserve the scope limitation the variables in the callback function.

1https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Other_notes

2https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Legacy_Internet_Explorer_and_attachEvent)

3https://developer.mozilla.org/en-US/docs/Web/API/Window/DOMContentLoaded_event#Browser_compatibility

\$\endgroup\$
1
  • 1
    \$\begingroup\$ Given the bounty to you for the most indepth answer on here... Thanks for the input, will use some of your suggestion to build upon my next iterations & code attempts. \$\endgroup\$
    – Ryan Stone
    Mar 14, 2020 at 2:56
0
\$\begingroup\$

This is the Secondary Attempt that I have made & will Add more to it when I have learnt a bit more JS

   let webAnimationsSupport = (window.Element.prototype.animate !== undefined);
   let rafSupport = (window.requestAnimationFrame !== undefined);
   let cssFallback = false;

   switch(webAnimationsSupport ? 'true' : 'false'){
       case "true":
            // Run .animate() functions as normal via Callbacks.
            console.log('.animate(); = true');
       break;
       case "false":
            console.log('.animate(); Support = false');
            animeFallBack();
            // Move onto requestAnimationFrame();
       break;

       default:
           // Default to Css Fallback. ie ``Add Back in the Classes`` That governed the original Animation.
   }
   function animeFallBack(){
       switch(rafSupport ? 'true' : 'false'){
           case "true":
                // .animate Not Supported Fallback to `request animation frame`.
                // Add Callback here which holds RAF Anime Code.
                console.log('.raf(); = true');
           break;
           case "false":
                // Fallback option or alert enable Js
                console.log('.raf(); = false');
                let requestAnimationFrame = (
                    window.requestAnimationFrame ||
                    window.mozRequestAnimationFrame ||
                    window.webkitRequestAnimationFrame ||
                    window.msRequestAnimationFrame ||
                    function(callback) {
                        return window.setTimeout(callback, 1000 / 60)
                    }
                );
           break;

           default:
                // Default to Css Fallback.
       }
   }
```
\$\endgroup\$
4
  • 1
    \$\begingroup\$ What's up with those switchstatements? The comments seem to indicate that the default clause can be executed, however that is not the case, so they could just be removed. The mapping to "true"/"false" strings is strange. Boolean values can be used just as well: switch (!!rafSupport) { case true: /* ... */ case false: /*...*/. But why not just use an if/else statement? \$\endgroup\$
    – RoToRa
    Mar 9, 2020 at 10:12
  • \$\begingroup\$ @RoToRa Only tried to use switch statement because i'm trying to learn Javascript & these were something that I had not learnt yet. Maybe needs refactoring alot but I prefer them to if/else's for some reason. \$\endgroup\$
    – Ryan Stone
    Mar 9, 2020 at 12:18
  • \$\begingroup\$ @RotoRa you could (and should) add those comments in an answer to the follow-up question where this code appears. \$\endgroup\$ Mar 9, 2020 at 17:38
  • \$\begingroup\$ @RoToRa this secondary attempt was just a work in progress & still needs refactoring. Probably Might get it correct on the 3rd, 4th, 5th, 6th or 8th attempt. \$\endgroup\$
    – Ryan Stone
    Mar 9, 2020 at 20:10

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.