javascriptangularjs

Wait until scrollTo is complete before running a command


I've got an AngularJS app, and I have a smooth scrolling directive to force the page to scroll to the bottom. I want the command to only run after the scrolling has finished. You can see that at the moment I run the scrolling function and then I run $('#comment-input').focus(); to focus on an element. I want to change it so this is only ran after the scrolling. I know I need to implement a callback but I can't figure out where to implement it.

(function() {

    var app = angular.module('myApp');

    app.service('anchorSmoothScroll', function(){

        this.scrollTo = function(eID) {

            // This scrolling function 
            // is from http://www.itnewb.com/tutorial/Creating-the-Smooth-Scroll-Effect-with-JavaScript

            var startY = currentYPosition();
            var stopY = elmYPosition(eID);
            var distance = stopY > startY ? stopY - startY : startY - stopY;
            if (distance < 100) {
                scrollTo(0, stopY); return;
            }
            var speed = Math.round(distance / 100);
            if (speed >= 20) speed = 20;
            var step = Math.round(distance / 25);
            var leapY = stopY > startY ? startY + step : startY - step;
            var timer = 0;
            if (stopY > startY) {
                for ( var i=startY; i<stopY; i+=step ) {
                    setTimeout("window.scrollTo(0, "+leapY+")", timer * speed);
                    leapY += step; if (leapY > stopY) leapY = stopY; timer++;
                } return;
            }
            for ( var i=startY; i>stopY; i-=step ) {
                setTimeout("window.scrollTo(0, "+leapY+")", timer * speed);
                leapY -= step; if (leapY < stopY) leapY = stopY; timer++;
            }

            function currentYPosition() {
                // Firefox, Chrome, Opera, Safari
                if (self.pageYOffset) return self.pageYOffset;
                // Internet Explorer 6 - standards mode
                if (document.documentElement && document.documentElement.scrollTop)
                    return document.documentElement.scrollTop;
                // Internet Explorer 6, 7 and 8
                if (document.body.scrollTop) return document.body.scrollTop;
                return 0;
            }

            function elmYPosition(eID) {
                var elm = document.getElementById(eID);
                var y = elm.offsetTop;
                var node = elm;
                while (node.offsetParent && node.offsetParent != document.body) {
                    node = node.offsetParent;
                    y += node.offsetTop;
                } return y;
            }

        };

    });

    app.controller('TextareaController', ['$scope','$location', 'anchorSmoothScroll',
    function($scope, $location, anchorSmoothScroll) {

        $scope.gotoElement = function (eID){
          // set the location.hash to the id of
          // the element you wish to scroll to.
          $location.hash('bottom-div');

          // call $anchorScroll()
          anchorSmoothScroll.scrollTo(eID);

          $('#comment-input').focus();

        };

    }]);

}());

Solution

  • Get anchorSmoothScroll.scrollTo to return a promise (using $q) and then focus once the promise is fullfilled. Read here for more info on $q.

    anchorSmoothScroll.scrollTo(eID)
         .then(function() {
              $('#comment-input').focus();
         })
    

    EDIT: Your code was failing because you are doing scrollTo inside setTimeout and that's where you would have to resolve your promise. Now, next problem is that there are multiple setTimeouts (as it is inside a for loop), so there would be lots of promises, and that's where $q.all will help.

    I have setup a plunker for you, where I have updated one of the execution paths... Have a look here. In the console window, you will see that focusing is printer after scrolling has finished. To make it obvious, I have hard-coded the setTimeout interval to 2 seconds. I hope that helps.