Accéder au contenu principal

Dynamic Read This list

I recently stumbled upon a news website with a catchy reading list, and I knew on the spot I needed to get down to work to replicate that effect. You can find a live example of the original concept at the URL below:

http://www.thedailybeast.com/articles/2014/09/21/tea-leoni-isn-t-hillary-clinton-but-madam-secretary-may-be-her-benghazi.html

In case the URL is no longer valid as you're reading this, here is a screenshot of it. 




Basically, there are 5 articles per page. As you scroll the page, the reading list updates so that you can see the progress of your reading and how much is left. Note that the whole pagelet (the "Read THIS.list" box) has a fixed position and thus moves with the scrolling of the page.  

What do we need?

As usual, I recommend using jQuery (I've become addicted to it!) as it on the one hand highly simplifies development in JS, and on the second hand it will guarantee we can retrieve consistent height/offset values for all browsers at once.

Else than that, not much is actually needed, aside from a working brain :-) I went into this little project with the only picture in mind of what I wanted to achieve in the end, but basically, I just opened Notepad++ and started coding with no preparation. It took me about 1 hour to get the result below:

http://manuthommes.be/readthis/

How is it done?

Since the code is not obfuscated and all in clear JS, you can have a look as much as you want on the actual page, but here are a few hints/points of attention.

1. CSS
            #readThis{
                position:fixed;
                width:300x;
                height:100px;
                right:50px;
            }
            .progBar{
                width:300px;
                height:40px;
                background:transparent;
                margin:10px;
                border-top:1px solid #000;
            }   
            .filled{
                background:#ccc;
                width:0px;
                height:40px;
                float:left;       
            }       
            .hdline{width:350px;}   
            .section{width:1000px; background:#eee; margin:15px;}

- readThis is the ID of the whole ReadThis pagelet. It has a fixed position and will be positionned on the right of the page.
 - progBar is a class used to style the default progress bars (as they appear when the page loads and when no scrolling has happened yet)
- filled is a class used to define the style of the progress bars being filled in (when the scrolling happens)
- hdline is a class applying to the labels within the progress bars
- section gives some styling to the articles   ...all in all, nothing fancy!

2. Javascript
            jQuery(document).ready(function(){
                $(window).scroll(function(){        calcScroll();    });
                $(".filled a").click(function(event){
                event.preventDefault();
                    $(window).scrollTop(($("div#"+$(this).attr("data")).offset().top)+1);
                });
            });

Quick hook to make sure scrolling the page or clicking on a header does what we want. Might not be fully needed, but several times the script happens to not execute properly, so I just wanted to make myself certain I was in control of everything.
Note that if you want to implement smooth scrolling for local anchors, you anyway have to hook <a> elements to prevent the default behaviour of jumping from A to B instantly.
            function rndwidth(n)
            {    if(n<10)n=0;if(n>290)n=300;    return n;    }

The above function will be called to round up or down the values retrieved from the scrolling, in case the user jumps too fast. Say for example that your first progress bar is filled in up to 348px out of 350 and the user suddenly move the scroll bar very fast, the actual script will not be able to cater for that and a gap will be visible. What we're doing here is correcting the low and high values to make sure that past a certain value, the progress will be fully filled in (or not at all). You can just comment out the content of the function to see what it does...but don't forget to leave the return value.

And now the core of the script:
            function calcScroll()
            {
                    var tAS=$("#a").offset().top, tAE=$("#a").height(), m=tAS+tAE;
                    var tBS=$("#b").offset().top, tBE=$("#b").height(), n=tBS+tBE;
                    var tCS=$("#c").offset().top, tCE=$("#c").height(), o=tCS+tCE;
                    var tDS=$("#d").offset().top, tDE=$("#d").height(), p=tDS+tDE;   
                    var sp=$(window).scrollTop();
// sp = position of the scrolling bar
// tAS = position of the #a element on the page (article 1)
// tAE = height of the article
// m = end position of the article (start position + height, since all of these are px values)
// A, B, C and D relate to the various article             
                    if(sp<tDS)$("#title4 .filled").width(0);
                    if(sp<tCS)$("#title3 .filled").width(0);
                    if(sp<tBS)$("#title2 .filled").width(0);
                    if(sp<tAS)$("#title1 .filled").width(0);
                   
// when the function is called, we make sure to reset every scrollbar if the scrolling position is smaller than the offet position)
                    if(sp>=tAS && sp<m){
                        var sp=parseInt(sp-tAS), h=m-$("#a").offset().top;
                        var nwidth=parseInt((300/h)*sp);
                        $("#title1 .filled").width(rndwidth(nwidth));
                    }
// if the scrolling position is bigger than or equal to the start position (offset) of article 1 AND if the scrolling position is smaller than the end of the article, we redefine 'sp' = scrolling position minus start position (parsed as integer since we're dealing wih pixels), and 'h' = end position of article minus start of article (...I'm realizing now as I'm writing this is basically the height of the article...but whatever!)
// quick rules of three : 'nwidth' (length in pixels of the filling of the progress bar) = 300 (length of the progress bar by default) divided by 'h', multiplied by 'sp'
// we then define the width of the "filled" class with the value of 'nwidth'
// in a nutshell: if and only if the scrolling position of the page is somewhere after the beginning of the article and the end of the article, we capture it and apply a simple rule of three: start position of article up to end of the article = 100% of the progress bar (meaning 300px), thus you need to recalculate a proper scaled value for the progress bar.
// ...again, this piece will be repeated for the 4 articles...but surely there's a way a cleaner way to do this in order to place everything in a single loop :-) Else just mind the use of variables and do not get mixed up! 
                    if(sp>=tBS && sp<n){       
                            $("#title1 .filled").width(300);
                            var sp=parseInt(sp-tBS), h=n-$("#b").offset().top;
                            var nwidth=parseInt((300/h)*sp);
                            if(nwidth<10)nwidth=0;if(nwidth>290)nwidth=300;
                            $("#title2 .filled").width(rndwidth(nwidth));
                    }
                    if(sp>=tCS && sp<o){   
                            $("#title2 .filled, #title1 .filled").width(300);
                            var sp=parseInt(sp-tCS), h=o-$("#c").offset().top;
                            var nwidth=parseInt((300/h)*sp);
                            $("#title3 .filled").width(rndwidth(nwidth));
                    }
                    if(sp>=tDS && sp<p){       
                            $("#title3 .filled,#title2 .filled,#title1 .filled").width(300);
                            var sp=parseInt(sp-tDS), h=p-$("#d").offset().top;
                            var nwidth=parseInt((300/h)*sp);
                            $("#title4 .filled").width(rndwidth(nwidth));
                    }
                   
            }
The HTML part is really straight-forward and simply consist in properly nesting the various elements (ReadThis object, progress bars, bars filled in, anchors...).

To do

- if the page is too short and there is no extra space after the last article, the last progress bar won't be filled in completely; this happens because a progress bar will be fully filled in only when the top of the screen has passed the offset position of the end of the article -> if the scrolling stops as still 50% of the article is visible, only 50% of the progress will be filled in.
- implement smooth scrolling
- clean the code to have only one function (avoid repeating pieces of code for all articles); basically, you just need to put a proper ID/class to your various HTML elements, then use a for loop to get all variables in an array, and from that point on it's almost easier since you won't get confused with  plenty of different variables :-) 

That's all, folks!


Commentaires

Posts les plus consultés de ce blog

A binary clock in Javascript

Just a short post to share a tiny project I recently worked on: a binary clock. The script is written in pure Javascript (no jQuery for a change), and the design of the buttons was rapidly made in Photoshop, so nothing fancy...but I really wanted to create this :-) http://manuthommes.be/toolbox/binaryclock/ As usual, the code is not obfuscated so you can have a look and feel free to copy/edit.

HTML tables and auto-sizing cell heights : a workaround

Although this was common practice in the early days of the Web, working with HTML tables when it comes to building a webpage layout should be avoided as much as possible nowadays. CSS offers all the tools that are needed to create perfectly dynamic and flexible layouts, so you really should only rely on HTML tables to present actual data. This being said, it happens that you don't have the choice, and that this just what I've learned recently in my work. In the context of a client project, the framework I've had to use (Peoplesoft, an HRIS system) indeed generates HTML pages presenting a structure quite "2000"-ish, consisting in a complex nesting of HTML tables. Moreover, each time you add a component on the page (via the system itself, not when hardcoding it), a system TABLE is create to wrap the object. This can cause obviously a lot of problems, but in this case, my problem was quite specific. A few pages of the system use a well-defined table layout :...